spanner/ddl/ast.go (554 lines of code) (raw):
// Copyright 2020 Google LLC
//
// 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 ddl provides a go representation of Spanner DDL
// as well as helpers for building and manipulating Spanner DDL.
// We only implement enough DDL types to meet the needs of Spanner migration tool.
//
// Definitions are from
// https://cloud.google.com/spanner/docs/data-definition-language.
package ddl
import (
"fmt"
"math"
"sort"
"strconv"
"strings"
"github.com/GoogleCloudPlatform/spanner-migration-tool/common/constants"
"github.com/GoogleCloudPlatform/spanner-migration-tool/logger"
)
const (
// Types supported by Spanner with google_standard_sql (default) dialect.
// Bool represent BOOL type.
Bool string = "BOOL"
// Bytes represent BYTES type.
Bytes string = "BYTES"
// Date represent DATE type.
Date string = "DATE"
// Float64 represent FLOAT32 type.
Float32 string = "FLOAT32"
// Float64 represent FLOAT64 type.
Float64 string = "FLOAT64"
// Int64 represent INT64 type.
Int64 string = "INT64"
// String represent STRING type.
String string = "STRING"
// Timestamp represent TIMESTAMP type.
Timestamp string = "TIMESTAMP"
// Numeric represent NUMERIC type.
Numeric string = "NUMERIC"
// Json represent JSON type.
JSON string = "JSON"
// MaxLength is a sentinel for Type's Len field, representing the MAX value.
MaxLength = math.MaxInt64
// StringMaxLength represents maximum allowed STRING length.
StringMaxLength = 2621440
// BytesMaxLength represents maximum allowed BYTES length.
BytesMaxLength = 10485760
MaxNonKeyColumnLength = 1677721600
// Types specific to Spanner with postgresql dialect, when they differ from
// Spanner with google_standard_sql.
// PGBytea represent BYTEA type, which is BYTES type in PG.
PGBytea string = "BYTEA"
// PGFloat4 represents the FLOAT4 type, which is a float type in PG.
PGFloat4 string = "FLOAT4"
// PGFloat8 represent FLOAT8 type, which is double type in PG.
PGFloat8 string = "FLOAT8"
// PGInt8 respresent INT8, which is INT type in PG.
PGInt8 string = "INT8"
// PGVarchar represent VARCHAR, which is STRING type in PG.
PGVarchar string = "VARCHAR"
// PGTimestamptz represent TIMESTAMPTZ, which is TIMESTAMP type in PG.
PGTimestamptz string = "TIMESTAMPTZ"
// Jsonb represents the PG.JSONB type
PGJSONB string = "JSONB"
// PGMaxLength represents sentinel for Type's Len field in PG.
PGMaxLength = 2621440
)
var STANDARD_TYPE_TO_PGSQL_TYPEMAP = map[string]string{
Bytes: PGBytea,
Float32: PGFloat4,
Float64: PGFloat8,
Int64: PGInt8,
String: PGVarchar,
Timestamp: PGTimestamptz,
JSON: PGJSONB,
}
var PGSQL_TO_STANDARD_TYPE_TYPEMAP = map[string]string{
PGBytea: Bytes,
PGFloat4: Float32,
PGFloat8: Float64,
PGInt8: Int64,
PGVarchar: String,
PGTimestamptz: Timestamp,
PGJSONB: JSON,
}
// PGDialect keyword list
// Assumption is that this list PGSQL dialect uses the same keywords
var PGSQL_RESERVED_KEYWORD_LIST = []string{"ALL", "ANALYSE", "ANALYZE", "AND", "ANY", "ARRAY", "AS", "ASC", "ASYMMETRIC", "AUTHORIZATION", "BETWEEN", "BIGINT", "BINARY", "BIT", "BOOLEAN", "BOTH", "CASE", "CAST",
"CHAR", "CHARACTER", "CHECK", "COALESCE", "COLLATE", "COLLATION", "COLUMN", "CONCURRENTLY", "CONSTRAINT", "CREATE", "CROSS", "CURRENT_CATALOG", "CURRENT_DATE", "CURRENT_ROLE", "CURRENT_SCHEMA",
"CURRENT_TIME", "CURRENT_TIMESTAMP", "CURRENT_USER", "DEC", "DECIMAL", "DEFAULT", "DEFERRABLE", "DESC", "DISTINCT", "DO", "ELSE", "END", "EXCEPT", "EXISTS", "EXTRACT", "FALSE", "FETCH", "FLOAT", "FOR", "FOREIGN",
"FREEZE", "FROM", "FULL", "GRANT", "GREATEST", "GROUP", "GROUPING", "HAVING", "ILIKE", "IN", "INITIALLY", "INNER", "INOUT", "INT", "INTEGER", "INTERSECT", "INTERVAL", "INTO", "IS", "ISNULL", "JOIN", "LATERAL", "LEADING",
"LEAST", "LEFT", "LIKE", "LIMIT", "LOCALTIME", "LOCALTIMESTAMP", "NATIONAL", "NATURAL", "NCHAR", "NONE", "NORMALIZE", "NOT", "NOTNULL", "NULL", "NULLIF", "NUMERIC", "OFFSET", "ON", "ONLY", "OR", "ORDER", "OUT", "OUTER",
"OVERLAPS", "OVERLAY", "PLACING", "POSITION", "PRECISION", "PRIMARY", "REAL", "REFERENCES", "RETURNING", "RIGHT", "ROW", "SELECT", "SESSION_USER", "SETOF", "SIMILAR", "SMALLINT", "SOME", "SUBSTRING", "SYMMETRIC",
"TABLE", "TABLESAMPLE", "THEN", "TIME", "TIMESTAMP", "TO", "TRAILING", "TREAT", "TRIM", "TRUE", "UNION", "UNIQUE", "USER", "USING", "VALUES", "VARCHAR", "VARIADIC", "VERBOSE", "WHEN", "WHERE", "WINDOW", "WITH",
"XMLATTRIBUTES", "XMLCONCAT", "XMLELEMENT", "XMLEXISTS", "XMLFOREST", "XMLNAMESPACES", "XMLPARSE", "XMLPI", "XMLROOT", "XMLSERIALIZE", "XMLTABLE"}
// Type represents the type of a column.
//
// type:
// { BOOL | INT64 | FLOAT32 | FLOAT64 | STRING( length ) | BYTES( length ) | DATE | TIMESTAMP | NUMERIC }
type Type struct {
Name string
// Len encodes the following Spanner DDL definition:
// length:
// { int64_value | MAX }
Len int64
// IsArray represents if Type is an array_type or not
// When false, column has type T; when true, it is an array of type T.
IsArray bool
}
// PrintColumnDefType unparses the type encoded in a ColumnDef.
func (ty Type) PrintColumnDefType() string {
str := ty.Name
if ty.Name == String || ty.Name == Bytes {
str += "("
if ty.Len == MaxLength {
str += "MAX"
} else {
str += strconv.FormatInt(ty.Len, 10)
}
str += ")"
}
if ty.IsArray {
str = "ARRAY<" + str + ">"
}
return str
}
func GetPGType(spType Type) string {
pgType, ok := STANDARD_TYPE_TO_PGSQL_TYPEMAP[spType.Name]
if ok {
return pgType
}
return spType.Name
}
func (ty Type) PGPrintColumnDefType() string {
str := GetPGType(ty)
// PG doesn't support array types, and we don't expect to receive a type
// with IsArray set to true. In the unlikely event, set to string type.
if ty.IsArray {
str = PGVarchar
ty.Len = PGMaxLength
}
// PG doesn't support variable length Bytea and thus doesn't support
// setting length (or max length) for the Bytes.
if ty.Name == String || ty.IsArray {
str += "("
if ty.Len == MaxLength || ty.Len == PGMaxLength {
str += fmt.Sprintf("%v", PGMaxLength)
} else {
str += strconv.FormatInt(ty.Len, 10)
}
str += ")"
}
return str
}
// ColumnDef encodes the following DDL definition:
//
// column_def:
// column_name type [NOT NULL] [options_def]
type ColumnDef struct {
Name string
T Type
NotNull bool
Comment string
Id string
AutoGen AutoGenCol
DefaultValue DefaultValue
}
// Config controls how AST nodes are printed (aka unparsed).
type Config struct {
Comments bool // If true, print comments.
ProtectIds bool // If true, table and col names are quoted using backticks (avoids reserved-word issue).
Tables bool // If true, print tables
ForeignKeys bool // If true, print foreign key constraints.
SpDialect string
Source string // SourceDB information for determining case-sensitivity handling for PGSQL
}
func isIdentifierReservedInPG(identifier string) bool {
for _, KEYWORD := range PGSQL_RESERVED_KEYWORD_LIST {
if strings.EqualFold(KEYWORD, identifier) {
return true
}
}
return false
}
func isSourceCaseSensitive(source string) bool {
switch source {
case constants.POSTGRES, constants.PGDUMP:
return true
default:
return false
}
}
func (c Config) quote(s string) string {
if c.ProtectIds {
if c.SpDialect == constants.DIALECT_POSTGRESQL {
if isIdentifierReservedInPG(s) || isSourceCaseSensitive(c.Source) {
return "\"" + s + "\""
} else {
return s
}
} else {
return "`" + s + "`"
}
}
return s
}
// PrintColumnDef unparses ColumnDef and returns it as well as any ColumnDef
// comment. These are returned as separate strings to support formatting
// needs of PrintCreateTable.
func (cd ColumnDef) PrintColumnDef(c Config) (string, string) {
var s string
if c.SpDialect == constants.DIALECT_POSTGRESQL {
s = fmt.Sprintf("%s %s", c.quote(cd.Name), cd.T.PGPrintColumnDefType())
if cd.NotNull {
s += " NOT NULL "
}
s += cd.DefaultValue.PGPrintDefaultValue(cd.T)
s += cd.AutoGen.PGPrintAutoGenCol()
} else {
s = fmt.Sprintf("%s %s", c.quote(cd.Name), cd.T.PrintColumnDefType())
if cd.NotNull {
s += " NOT NULL "
}
s += cd.DefaultValue.PrintDefaultValue(cd.T)
s += cd.AutoGen.PrintAutoGenCol()
}
return s, cd.Comment
}
// IndexKey encodes the following DDL definition:
//
// primary_key:
// PRIMARY KEY ( [key_part, ...] )
// key_part:
// column_name [{ ASC | DESC }]
type IndexKey struct {
ColId string
Desc bool // Default order is ascending i.e. Desc = false.
Order int
}
type CheckConstraint struct {
Id string
Name string
Expr string
ExprId string
}
// PrintPkOrIndexKey unparses the primary or index keys.
func (idx IndexKey) PrintPkOrIndexKey(ct CreateTable, c Config) string {
col := c.quote(ct.ColDefs[idx.ColId].Name)
if idx.Desc {
return fmt.Sprintf("%s DESC", col)
}
// Don't print out ASC -- that's the default.
return col
}
// Foreignkey encodes the following DDL definition:
//
// [ CONSTRAINT constraint_name ]
// FOREIGN KEY ( column_name [, ... ] ) REFERENCES ref_table ( ref_column [, ... ] ) }
type Foreignkey struct {
Name string
ColIds []string
ReferTableId string
ReferColumnIds []string
Id string
OnDelete string
OnUpdate string
}
// InterleavedParent encodes the following DDL definition:
//
// INTERLEAVE IN PARENT parent_name ON DELETE delete_rule
type InterleavedParent struct {
Id string
OnDelete string
}
// PrintForeignKey unparses the foreign keys.
func (k Foreignkey) PrintForeignKey(c Config) string {
var cols, referCols []string
for i, col := range k.ColIds {
cols = append(cols, c.quote(col))
referCols = append(referCols, c.quote(k.ReferColumnIds[i]))
}
var s string
if k.Name != "" {
s = fmt.Sprintf("CONSTRAINT %s ", c.quote(k.Name))
}
s = s + fmt.Sprintf("FOREIGN KEY (%s) REFERENCES %s (%s)", strings.Join(cols, ", "), c.quote(k.ReferTableId), strings.Join(referCols, ", "))
if k.OnDelete != "" {
s = s + fmt.Sprintf(" ON DELETE %s", k.OnDelete)
}
return s
}
// CreateTable encodes the following DDL definition:
//
// create_table: CREATE TABLE table_name ([column_def, ...] ) primary_key [, cluster]
type CreateTable struct {
Name string
ColIds []string // Provides names and order of columns
ShardIdColumn string
ColDefs map[string]ColumnDef // Provides definition of columns (a map for simpler/faster lookup during type processing)
PrimaryKeys []IndexKey
ForeignKeys []Foreignkey
Indexes []CreateIndex
ParentTable InterleavedParent // if not empty, this table will be interleaved
CheckConstraints []CheckConstraint
Comment string
Id string
}
// PrintCreateTable unparses a CREATE TABLE statement.
func (ct CreateTable) PrintCreateTable(spSchema Schema, config Config) string {
var col []string
var colComment []string
var keys []string
for _, colId := range ct.ColIds {
s, c := ct.ColDefs[colId].PrintColumnDef(config)
s = "\t" + s + ","
col = append(col, s)
colComment = append(colComment, c)
}
n := maxStringLength(col)
var cols string
for i, c := range col {
cols += c
if config.Comments && len(colComment[i]) > 0 {
cols += strings.Repeat(" ", n-len(c)) + " -- " + colComment[i]
}
cols += "\n"
}
orderedPks := []IndexKey{}
orderedPks = append(orderedPks, ct.PrimaryKeys...)
sort.Slice(orderedPks, func(i, j int) bool {
return orderedPks[i].Order < orderedPks[j].Order
})
for _, p := range orderedPks {
keys = append(keys, p.PrintPkOrIndexKey(ct, config))
}
var tableComment string
if config.Comments && len(ct.Comment) > 0 {
tableComment = "--\n-- " + ct.Comment + "\n--\n"
}
var interleave string
if ct.ParentTable.Id != "" {
parent := spSchema[ct.ParentTable.Id].Name
if config.SpDialect == constants.DIALECT_POSTGRESQL {
// PG spanner only supports PRIMARY KEY() inside the CREATE TABLE()
// and thus INTERLEAVE follows immediately after closing brace.
interleave = " INTERLEAVE IN PARENT " + config.quote(parent)
} else {
interleave = ",\nINTERLEAVE IN PARENT " + config.quote(parent)
}
if ct.ParentTable.OnDelete != "" {
interleave = interleave + " ON DELETE " + ct.ParentTable.OnDelete
}
}
var checkString string
if len(ct.CheckConstraints) > 0 {
checkString = FormatCheckConstraints(ct.CheckConstraints, config.SpDialect)
} else {
checkString = ""
}
if len(keys) == 0 {
return fmt.Sprintf("%sCREATE TABLE %s (\n%s%s) %s", tableComment, config.quote(ct.Name), cols, checkString, interleave)
}
if config.SpDialect == constants.DIALECT_POSTGRESQL {
return fmt.Sprintf("%sCREATE TABLE %s (\n%s%s\tPRIMARY KEY (%s)\n)%s", tableComment, config.quote(ct.Name), cols, checkString, strings.Join(keys, ", "), interleave)
}
return fmt.Sprintf("%sCREATE TABLE %s (\n%s%s) PRIMARY KEY (%s)%s", tableComment, config.quote(ct.Name), cols, checkString, strings.Join(keys, ", "), interleave)
}
// CreateIndex encodes the following DDL definition:
//
// create index: CREATE [UNIQUE] [NULL_FILTERED] INDEX index_name ON table_name ( key_part [, ...] ) [ storing_clause ] [ , interleave_clause ]
type CreateIndex struct {
Name string
TableId string `json:"TableId"`
Unique bool
Keys []IndexKey
Id string
StoredColumnIds []string
// We have no requirements for null-filtered option and
// interleaving clauses yet, so we omit them for now.
}
type AutoGenCol struct {
Name string
// Type of autogenerated column, example, pre-defined(uuid) or user-defined(sequence)
GenerationType string
}
// DefaultValue represents a Default value.
type DefaultValue struct {
IsPresent bool
Value Expression
}
type Expression struct {
ExpressionId string
Statement string
}
func (dv DefaultValue) PrintDefaultValue(ty Type) string {
if !dv.IsPresent {
return ""
}
var value string
switch ty.Name {
case "FLOAT32", "NUMERIC", "BOOL", "BYTES":
value = fmt.Sprintf(" DEFAULT (CAST(%s AS %s))", dv.Value.Statement, ty.Name)
default:
value = " DEFAULT (" + dv.Value.Statement + ")"
}
return value
}
func (dv DefaultValue) PGPrintDefaultValue(ty Type) string {
if !dv.IsPresent {
return ""
}
var value string
switch GetPGType(ty) {
case "FLOAT8", "FLOAT4", "REAL", "NUMERIC", "DECIMAL", "BOOL", "BYTEA":
value = fmt.Sprintf(" DEFAULT (CAST(%s AS %s))", dv.Value.Statement, GetPGType(ty))
default:
value = " DEFAULT (" + dv.Value.Statement + ")"
}
return value
}
func (agc AutoGenCol) PrintAutoGenCol() string {
if agc.Name == constants.UUID && agc.GenerationType == "Pre-defined" {
return " DEFAULT (GENERATE_UUID())"
}
if agc.GenerationType == constants.SEQUENCE {
return fmt.Sprintf(" DEFAULT (GET_NEXT_SEQUENCE_VALUE(SEQUENCE %s))", agc.Name)
}
return ""
}
func (agc AutoGenCol) PGPrintAutoGenCol() string {
if agc.Name == constants.UUID && agc.GenerationType == "Pre-defined" {
return " DEFAULT (spanner.generate_uuid())"
}
if agc.GenerationType == constants.SEQUENCE {
return fmt.Sprintf(" DEFAULT NEXTVAL('%s')", agc.Name)
}
return ""
}
// PrintCreateIndex unparses a CREATE INDEX statement.
func (ci CreateIndex) PrintCreateIndex(ct CreateTable, c Config) string {
var keys []string
orderedKeys := []IndexKey{}
orderedKeys = append(orderedKeys, ci.Keys...)
sort.Slice(orderedKeys, func(i, j int) bool {
return orderedKeys[i].Order < orderedKeys[j].Order
})
for _, p := range orderedKeys {
keys = append(keys, p.PrintPkOrIndexKey(ct, c))
}
var unique, stored, storingClause string
if ci.Unique {
unique = "UNIQUE "
}
if c.SpDialect == constants.DIALECT_POSTGRESQL {
stored = "INCLUDE"
} else {
stored = "STORING"
}
if ci.StoredColumnIds != nil {
storedColumns := []string{}
for _, colId := range ci.StoredColumnIds {
if !isStoredColumnKeyPartOfPrimaryKey(ct, colId) {
storedColumns = append(storedColumns, c.quote(ct.ColDefs[colId].Name))
}
}
storingClause = fmt.Sprintf(" %s (%s)", stored, strings.Join(storedColumns, ", "))
}
return fmt.Sprintf("CREATE %sINDEX %s ON %s (%s)%s", unique, c.quote(ci.Name), c.quote(ct.Name), strings.Join(keys, ", "), storingClause)
}
// Checks if the colId is part of the primary of a table
// Used for detecting if a key needs to be skipped while creating the
// storing clause.
func isStoredColumnKeyPartOfPrimaryKey(ct CreateTable, colId string) bool {
for _, pkey := range ct.PrimaryKeys {
if colId == pkey.ColId {
return true
}
}
return false
}
// PrintForeignKeyAlterTable unparses the foreign keys using ALTER TABLE.
func (k Foreignkey) PrintForeignKeyAlterTable(spannerSchema Schema, c Config, tableId string) string {
var cols, referCols []string
for i, col := range k.ColIds {
cols = append(cols, c.quote(spannerSchema[tableId].ColDefs[col].Name))
referCols = append(referCols, c.quote(spannerSchema[k.ReferTableId].ColDefs[k.ReferColumnIds[i]].Name))
}
var s string
if k.Name != "" {
s = fmt.Sprintf("CONSTRAINT %s ", c.quote(k.Name))
}
s = fmt.Sprintf("ALTER TABLE %s ADD %sFOREIGN KEY (%s) REFERENCES %s (%s)", c.quote(spannerSchema[tableId].Name), s, strings.Join(cols, ", "), c.quote(spannerSchema[k.ReferTableId].Name), strings.Join(referCols, ", "))
if k.OnDelete != "" {
s = s + fmt.Sprintf(" ON DELETE %s", k.OnDelete)
}
return s
}
// FormatCheckConstraints formats the check constraints in SQL syntax.
func FormatCheckConstraints(cks []CheckConstraint, dailect string) string {
var builder strings.Builder
for _, col := range cks {
if col.Name != "" {
builder.WriteString(fmt.Sprintf("\tCONSTRAINT %s CHECK %s,\n", col.Name, col.Expr))
} else {
builder.WriteString(fmt.Sprintf("\tCHECK %s,\n", col.Expr))
}
}
if builder.Len() > 0 {
// Trim the trailing comma and newline
result := builder.String()
if dailect == constants.DIALECT_GOOGLESQL {
return result[:len(result)-2] + "\n"
} else {
return result[:len(result)-2] + ",\n"
}
}
return ""
}
// Schema stores a map of table names and Tables.
type Schema map[string]CreateTable
// NewSchema creates a new Schema object.
func NewSchema() Schema {
return make(map[string]CreateTable)
}
// Tables are ordered in alphabetical order with one exception: interleaved
// tables appear after the definition of their parent table.
//
// TODO: Move this method to mapping.go and preserve the table names in sorted
// order in conv so that we don't need to order the table names multiple times.
func GetSortedTableIdsBySpName(s Schema) []string {
var tableNames, sortedTableNames, sortedTableIds []string
tableNameIdMap := map[string]string{}
for _, t := range s {
tableNames = append(tableNames, t.Name)
tableNameIdMap[t.Name] = t.Id
}
logger.Log.Debug(fmt.Sprintf("getting sorted table ids by table name: %s", tableNames))
sort.Strings(tableNames)
tableQueue := tableNames
tableAdded := make(map[string]bool)
for len(tableQueue) > 0 {
tableName := tableQueue[0]
table := s[tableNameIdMap[tableName]]
tableQueue = tableQueue[1:]
parentTableExists := false
if table.ParentTable.Id != "" {
_, parentTableExists = s[table.ParentTable.Id]
}
// Add table t if either:
// a) t is not interleaved in another table, or
// b) t is interleaved in another table and that table has already been added to the list.
if table.ParentTable.Id == "" || tableAdded[s[table.ParentTable.Id].Name] || !parentTableExists {
sortedTableNames = append(sortedTableNames, tableName)
tableAdded[tableName] = true
} else {
// We can't add table t now because its parent hasn't been added.
// Add it at end of tables and we'll try again later.
// We might need multiple iterations to add chains of interleaved tables,
// but we will always make progress because interleaved tables can't
// have cycles. In principle this could be O(n^2), but in practice chains
// of interleaved tables are small.
tableQueue = append(tableQueue, tableName)
}
}
for _, tableName := range sortedTableNames {
sortedTableIds = append(sortedTableIds, tableNameIdMap[tableName])
}
return sortedTableIds
}
// GetDDL returns the string representation of Spanner schema represented by Schema struct.
// Tables are printed in alphabetical order with one exception: interleaved
// tables are potentially out of order since they must appear after the
// definition of their parent table.
func GetDDL(c Config, tableSchema Schema, sequenceSchema map[string]Sequence) []string {
var ddl []string
for _, seq := range sequenceSchema {
if c.SpDialect == constants.DIALECT_POSTGRESQL {
ddl = append(ddl, seq.PGPrintSequence(c))
} else {
ddl = append(ddl, seq.PrintSequence(c))
}
}
tableIds := GetSortedTableIdsBySpName(tableSchema)
if c.Tables {
for _, tableId := range tableIds {
ddl = append(ddl, tableSchema[tableId].PrintCreateTable(tableSchema, c))
for _, index := range tableSchema[tableId].Indexes {
ddl = append(ddl, index.PrintCreateIndex(tableSchema[tableId], c))
}
}
}
// Append foreign key constraints to DDL.
// We always use alter table statements for foreign key constraints.
// The alternative of putting foreign key constraints in-line as part of create
// table statements is tricky because of table order (need to define tables
// before they are referenced by foreign key constraints) and the possibility
// of circular foreign keys definitions. We opt for simplicity.
if c.ForeignKeys {
for _, t := range tableIds {
for _, fk := range tableSchema[t].ForeignKeys {
ddl = append(ddl, fk.PrintForeignKeyAlterTable(tableSchema, c, t))
}
}
}
return ddl
}
// CheckInterleaved checks if schema contains interleaved tables.
func (s Schema) CheckInterleaved() bool {
for _, table := range s {
if table.ParentTable.Id != "" {
return true
}
}
return false
}
func maxStringLength(s []string) int {
n := 0
for _, x := range s {
if len(x) > n {
n = len(x)
}
}
return n
}
type Sequence struct {
Id string
Name string
SequenceKind string
SkipRangeMin string
SkipRangeMax string
StartWithCounter string
ColumnsUsingSeq map[string][]string
}
func (seq Sequence) PrintSequence(c Config) string {
var options []string
if seq.SequenceKind != "" {
if seq.SequenceKind == "BIT REVERSED POSITIVE" {
options = append(options, "sequence_kind='bit_reversed_positive'")
}
}
if seq.SkipRangeMin != "" {
options = append(options, fmt.Sprintf("skip_range_min = %s", seq.SkipRangeMin))
}
if seq.SkipRangeMax != "" {
options = append(options, fmt.Sprintf("skip_range_max = %s", seq.SkipRangeMax))
}
if seq.StartWithCounter != "" {
options = append(options, fmt.Sprintf("start_with_counter = %s", seq.StartWithCounter))
}
seqDDL := fmt.Sprintf("CREATE SEQUENCE %s", c.quote(seq.Name))
if len(options) > 0 {
seqDDL += " OPTIONS (" + strings.Join(options, ", ") + ") "
}
return seqDDL
}
func (seq Sequence) PGPrintSequence(c Config) string {
var options []string
if seq.SequenceKind != "" {
if seq.SequenceKind == "BIT REVERSED POSITIVE" {
options = append(options, " BIT_REVERSED_POSITIVE")
}
}
if seq.SkipRangeMax != "" && seq.SkipRangeMin != "" {
options = append(options, fmt.Sprintf("SKIP RANGE %s %s", seq.SkipRangeMin, seq.SkipRangeMax))
}
if seq.StartWithCounter != "" {
options = append(options, fmt.Sprintf("START COUNTER WITH %s", seq.StartWithCounter))
}
seqDDL := fmt.Sprintf("CREATE SEQUENCE %s", c.quote(seq.Name))
if len(options) > 0 {
seqDDL += strings.Join(options, " ")
}
return seqDDL
}