Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled
506 lines
12 KiB
Go
506 lines
12 KiB
Go
package catalog
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
|
"github.com/sqlc-dev/sqlc/internal/sql/sqlerr"
|
|
)
|
|
|
|
// Table describes a relational database table
|
|
//
|
|
// A database table is a collection of related data held in a table format within a database.
|
|
// It consists of columns and rows.
|
|
type Table struct {
|
|
Rel *ast.TableName
|
|
Columns []*Column
|
|
Comment string
|
|
}
|
|
|
|
func checkMissing(err error, missingOK bool) error {
|
|
var serr *sqlerr.Error
|
|
if errors.As(err, &serr) {
|
|
if serr.Err == sqlerr.NotFound && missingOK {
|
|
return nil
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (table *Table) isExistColumn(cmd *ast.AlterTableCmd) (int, error) {
|
|
for i, c := range table.Columns {
|
|
if c.Name == *cmd.Name {
|
|
return i, nil
|
|
}
|
|
}
|
|
if !cmd.MissingOk {
|
|
return -1, sqlerr.ColumnNotFound(table.Rel.Name, *cmd.Name)
|
|
}
|
|
// Missing column is allowed
|
|
return -1, nil
|
|
}
|
|
|
|
func (c *Catalog) addColumn(table *Table, cmd *ast.AlterTableCmd) error {
|
|
for _, c := range table.Columns {
|
|
if c.Name == cmd.Def.Colname {
|
|
if !cmd.MissingOk {
|
|
return sqlerr.ColumnExists(table.Rel.Name, cmd.Def.Colname)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
tc, err := c.defineColumn(table.Rel, cmd.Def)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
table.Columns = append(table.Columns, tc)
|
|
return nil
|
|
}
|
|
|
|
func (table *Table) alterColumnType(cmd *ast.AlterTableCmd) error {
|
|
index, err := table.isExistColumn(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if index >= 0 {
|
|
table.Columns[index].Type = *cmd.Def.TypeName
|
|
table.Columns[index].IsArray = cmd.Def.IsArray
|
|
table.Columns[index].ArrayDims = cmd.Def.ArrayDims
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Catalog) dropColumn(table *Table, cmd *ast.AlterTableCmd) error {
|
|
index, err := table.isExistColumn(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if index < 0 {
|
|
return nil
|
|
}
|
|
col := table.Columns[index]
|
|
if col.linkedType {
|
|
drop := &ast.DropTypeStmt{
|
|
Types: []*ast.TypeName{
|
|
&col.Type,
|
|
},
|
|
}
|
|
if err := c.dropType(drop); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
table.Columns = append(table.Columns[:index], table.Columns[index+1:]...)
|
|
return nil
|
|
}
|
|
|
|
func (table *Table) dropNotNull(cmd *ast.AlterTableCmd) error {
|
|
index, err := table.isExistColumn(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if index >= 0 {
|
|
table.Columns[index].IsNotNull = false
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (table *Table) setNotNull(cmd *ast.AlterTableCmd) error {
|
|
index, err := table.isExistColumn(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if index >= 0 {
|
|
table.Columns[index].IsNotNull = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Column describes a set of data values of a particular type in a relational database table
|
|
//
|
|
// TODO: Should this just be ast Nodes?
|
|
type Column struct {
|
|
Name string
|
|
Type ast.TypeName
|
|
IsNotNull bool
|
|
IsUnsigned bool
|
|
IsArray bool
|
|
ArrayDims int
|
|
Comment string
|
|
Length *int
|
|
|
|
linkedType bool
|
|
}
|
|
|
|
// An interface is used to resolve a circular import between the catalog and compiler packages.
|
|
// The createView function requires access to functions in the compiler package to parse the SELECT
|
|
// statement that defines the view.
|
|
type columnGenerator interface {
|
|
OutputColumns(node ast.Node) ([]*Column, error)
|
|
}
|
|
|
|
func (c *Catalog) getTable(tableName *ast.TableName) (*Schema, *Table, error) {
|
|
schemaName := tableName.Schema
|
|
if schemaName == "" {
|
|
schemaName = c.DefaultSchema
|
|
}
|
|
var schema *Schema
|
|
for i := range c.Schemas {
|
|
if c.Schemas[i].Name == schemaName {
|
|
schema = c.Schemas[i]
|
|
break
|
|
}
|
|
}
|
|
if schema == nil {
|
|
return nil, nil, sqlerr.SchemaNotFound(schemaName)
|
|
}
|
|
table, _, err := schema.getTable(tableName)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return schema, table, nil
|
|
}
|
|
|
|
func isStmtImplemented(stmt *ast.AlterTableStmt) bool {
|
|
var implemented bool
|
|
for _, item := range stmt.Cmds.Items {
|
|
switch cmd := item.(type) {
|
|
case *ast.AlterTableCmd:
|
|
switch cmd.Subtype {
|
|
case ast.AT_AddColumn:
|
|
implemented = true
|
|
case ast.AT_AlterColumnType:
|
|
implemented = true
|
|
case ast.AT_DropColumn:
|
|
implemented = true
|
|
case ast.AT_DropNotNull:
|
|
implemented = true
|
|
case ast.AT_SetNotNull:
|
|
implemented = true
|
|
}
|
|
}
|
|
}
|
|
return implemented
|
|
}
|
|
|
|
func (c *Catalog) alterTable(stmt *ast.AlterTableStmt) error {
|
|
if !isStmtImplemented(stmt) {
|
|
return nil
|
|
}
|
|
_, table, err := c.getTable(stmt.Table)
|
|
if err != nil {
|
|
return checkMissing(err, stmt.MissingOk)
|
|
}
|
|
for _, item := range stmt.Cmds.Items {
|
|
switch cmd := item.(type) {
|
|
case *ast.AlterTableCmd:
|
|
switch cmd.Subtype {
|
|
case ast.AT_AddColumn:
|
|
if err := c.addColumn(table, cmd); err != nil {
|
|
return err
|
|
}
|
|
case ast.AT_AlterColumnType:
|
|
if err := table.alterColumnType(cmd); err != nil {
|
|
return err
|
|
}
|
|
case ast.AT_DropColumn:
|
|
if err := c.dropColumn(table, cmd); err != nil {
|
|
return err
|
|
}
|
|
case ast.AT_DropNotNull:
|
|
if err := table.dropNotNull(cmd); err != nil {
|
|
return err
|
|
}
|
|
case ast.AT_SetNotNull:
|
|
if err := table.setNotNull(cmd); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Catalog) alterTableSetSchema(stmt *ast.AlterTableSetSchemaStmt) error {
|
|
ns := stmt.Table.Schema
|
|
if ns == "" {
|
|
ns = c.DefaultSchema
|
|
}
|
|
oldSchema, err := c.getSchema(ns)
|
|
if err != nil {
|
|
return checkMissing(err, stmt.MissingOk)
|
|
}
|
|
tbl, idx, err := oldSchema.getTable(stmt.Table)
|
|
if err != nil {
|
|
return checkMissing(err, stmt.MissingOk)
|
|
}
|
|
tbl.Rel.Schema = *stmt.NewSchema
|
|
newSchema, err := c.getSchema(*stmt.NewSchema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, _, err := newSchema.getTable(stmt.Table); err == nil {
|
|
return sqlerr.RelationExists(stmt.Table.Name)
|
|
}
|
|
oldSchema.Tables = append(oldSchema.Tables[:idx], oldSchema.Tables[idx+1:]...)
|
|
newSchema.Tables = append(newSchema.Tables, tbl)
|
|
return nil
|
|
}
|
|
|
|
func (c *Catalog) createTable(stmt *ast.CreateTableStmt) error {
|
|
// A virtual table's module is the engine's word for the extension it
|
|
// needs, the way CREATE EXTENSION is PostgreSQL's.
|
|
if stmt.Using != "" {
|
|
if err := c.loadExtension(stmt.Using); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
ns := stmt.Name.Schema
|
|
if ns == "" {
|
|
ns = c.DefaultSchema
|
|
}
|
|
schema, err := c.getSchema(ns)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, _, err = schema.getTable(stmt.Name)
|
|
if err == nil && stmt.IfNotExists {
|
|
return nil
|
|
} else if err == nil {
|
|
return sqlerr.RelationExists(stmt.Name.Name)
|
|
}
|
|
|
|
tbl := Table{Rel: stmt.Name, Comment: stmt.Comment}
|
|
coltype := make(map[string]ast.TypeName) // used to check for duplicate column names
|
|
seen := make(map[string]bool) // used to check for duplicate column names
|
|
for _, inheritTable := range stmt.Inherits {
|
|
t, _, err := schema.getTable(inheritTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// check and ignore duplicate columns
|
|
for _, col := range t.Columns {
|
|
if notNull, ok := seen[col.Name]; ok {
|
|
seen[col.Name] = notNull || col.IsNotNull
|
|
if a, ok := coltype[col.Name]; ok {
|
|
if !sameType(&a, &col.Type) {
|
|
return fmt.Errorf("column %q has a type conflict", col.Name)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
seen[col.Name] = col.IsNotNull
|
|
coltype[col.Name] = col.Type
|
|
tbl.Columns = append(tbl.Columns, col)
|
|
}
|
|
}
|
|
|
|
if stmt.ReferTable != nil {
|
|
_, original, err := c.getTable(stmt.ReferTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, col := range original.Columns {
|
|
newCol := *col // make a copy, so changes to the ReferTable don't propagate
|
|
tbl.Columns = append(tbl.Columns, &newCol)
|
|
}
|
|
}
|
|
|
|
for _, col := range stmt.Cols {
|
|
// The legacy catalog has no notion of a hidden column, so it drops
|
|
// them rather than list them on the table.
|
|
if col.IsHidden {
|
|
continue
|
|
}
|
|
if notNull, ok := seen[col.Colname]; ok {
|
|
seen[col.Colname] = notNull || col.IsNotNull
|
|
if a, ok := coltype[col.Colname]; ok {
|
|
if !sameType(&a, col.TypeName) {
|
|
return fmt.Errorf("column %q has a type conflict", col.Colname)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
tc, err := c.defineColumn(stmt.Name, col)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tbl.Columns = append(tbl.Columns, tc)
|
|
}
|
|
|
|
// If one of the merged columns was not null, mark the column as not null
|
|
for i := range tbl.Columns {
|
|
if notNull, ok := seen[tbl.Columns[i].Name]; ok {
|
|
tbl.Columns[i].IsNotNull = notNull
|
|
}
|
|
}
|
|
|
|
schema.Tables = append(schema.Tables, &tbl)
|
|
return nil
|
|
}
|
|
|
|
func (c *Catalog) defineColumn(table *ast.TableName, col *ast.ColumnDef) (*Column, error) {
|
|
tc := &Column{
|
|
Name: col.Colname,
|
|
Type: *col.TypeName,
|
|
IsNotNull: col.IsNotNull,
|
|
IsUnsigned: col.IsUnsigned,
|
|
IsArray: col.IsArray,
|
|
ArrayDims: col.ArrayDims,
|
|
Comment: col.Comment,
|
|
Length: col.Length,
|
|
}
|
|
if col.Vals != nil {
|
|
typeName := ast.TypeName{
|
|
Name: fmt.Sprintf("%s_%s", table.Name, col.Colname),
|
|
}
|
|
s := &ast.CreateEnumStmt{TypeName: &typeName, Vals: col.Vals}
|
|
if err := c.createEnum(s); err != nil {
|
|
return nil, err
|
|
}
|
|
tc.Type = typeName
|
|
tc.linkedType = true
|
|
}
|
|
return tc, nil
|
|
}
|
|
|
|
func (c *Catalog) dropTable(stmt *ast.DropTableStmt) error {
|
|
for _, name := range stmt.Tables {
|
|
ns := name.Schema
|
|
if ns == "" {
|
|
ns = c.DefaultSchema
|
|
}
|
|
schema, err := c.getSchema(ns)
|
|
if errors.Is(err, sqlerr.NotFound) && stmt.IfExists {
|
|
continue
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
tbl, idx, err := schema.getTable(name)
|
|
if errors.Is(err, sqlerr.NotFound) && stmt.IfExists {
|
|
continue
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
drop := &ast.DropTypeStmt{}
|
|
for _, col := range tbl.Columns {
|
|
if !col.linkedType {
|
|
continue
|
|
}
|
|
drop.Types = append(drop.Types, &col.Type)
|
|
}
|
|
if err := c.dropType(drop); err != nil {
|
|
return err
|
|
}
|
|
|
|
schema.Tables = append(schema.Tables[:idx], schema.Tables[idx+1:]...)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Catalog) renameColumn(stmt *ast.RenameColumnStmt) error {
|
|
_, tbl, err := c.getTable(stmt.Table)
|
|
if err != nil {
|
|
return checkMissing(err, stmt.MissingOk)
|
|
}
|
|
idx := -1
|
|
for i := range tbl.Columns {
|
|
if tbl.Columns[i].Name == stmt.Col.Name {
|
|
idx = i
|
|
}
|
|
if tbl.Columns[i].Name == *stmt.NewName {
|
|
return sqlerr.ColumnExists(tbl.Rel.Name, *stmt.NewName)
|
|
}
|
|
}
|
|
if idx == -1 {
|
|
return sqlerr.ColumnNotFound(tbl.Rel.Name, stmt.Col.Name)
|
|
}
|
|
tbl.Columns[idx].Name = *stmt.NewName
|
|
|
|
if tbl.Columns[idx].linkedType {
|
|
name := fmt.Sprintf("%s_%s", tbl.Rel.Name, *stmt.NewName)
|
|
rename := &ast.RenameTypeStmt{
|
|
Type: &tbl.Columns[idx].Type,
|
|
NewName: &name,
|
|
}
|
|
if err := c.renameType(rename); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Catalog) renameTable(stmt *ast.RenameTableStmt) error {
|
|
sch, tbl, err := c.getTable(stmt.Table)
|
|
if err != nil {
|
|
return checkMissing(err, stmt.MissingOk)
|
|
}
|
|
if _, _, err := sch.getTable(&ast.TableName{Name: *stmt.NewName}); err == nil {
|
|
return sqlerr.RelationExists(*stmt.NewName)
|
|
}
|
|
if stmt.NewName != nil {
|
|
tbl.Rel.Name = *stmt.NewName
|
|
}
|
|
|
|
for idx := range tbl.Columns {
|
|
if tbl.Columns[idx].linkedType {
|
|
name := fmt.Sprintf("%s_%s", *stmt.NewName, tbl.Columns[idx].Name)
|
|
rename := &ast.RenameTypeStmt{
|
|
Type: &tbl.Columns[idx].Type,
|
|
NewName: &name,
|
|
}
|
|
if err := c.renameType(rename); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Catalog) createTableAs(stmt *ast.CreateTableAsStmt, colGen columnGenerator) error {
|
|
cols, err := colGen.OutputColumns(stmt.Query)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
catName := ""
|
|
if stmt.Into.Rel.Catalogname != nil {
|
|
catName = *stmt.Into.Rel.Catalogname
|
|
}
|
|
schemaName := ""
|
|
if stmt.Into.Rel.Schemaname != nil {
|
|
schemaName = *stmt.Into.Rel.Schemaname
|
|
}
|
|
|
|
tbl := Table{
|
|
Rel: &ast.TableName{
|
|
Catalog: catName,
|
|
Schema: schemaName,
|
|
Name: *stmt.Into.Rel.Relname,
|
|
},
|
|
Columns: cols,
|
|
}
|
|
|
|
ns := tbl.Rel.Schema
|
|
if ns == "" {
|
|
ns = c.DefaultSchema
|
|
}
|
|
schema, err := c.getSchema(ns)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, _, err = schema.getTable(tbl.Rel)
|
|
if err == nil {
|
|
return sqlerr.RelationExists(tbl.Rel.Name)
|
|
}
|
|
|
|
schema.Tables = append(schema.Tables, &tbl)
|
|
|
|
return nil
|
|
}
|