Repository: ariga/atlas
Pull request #2139 (2023-09-30 07:46:32)

sql: add funcs/procs stubs


Patch:
diff --git a/schemahcl/spec.go b/schemahcl/spec.go
index ba87124e..bc4c105b 100644
--- a/schemahcl/spec.go
+++ b/schemahcl/spec.go
@@ -460,6 +460,16 @@ func RefAttr(k string, v *Ref) *Attr {
 	}
 }
 
+// RefValue is a helper method for constructing a cty.Value that contains a Ref value.
+func RefValue(v string) cty.Value {
+	return cty.CapsuleVal(ctyRefType, &Ref{V: v})
+}
+
+// TypeValue is a helper method for constructing a cty.Value that contains a Type value.
+func TypeValue(t *Type) cty.Value {
+	return cty.CapsuleVal(ctyTypeSpec, t)
+}
+
 // StringsAttr is a helper method for constructing *schemahcl.Attr instances that contain list strings.
 func StringsAttr(k string, vs ...string) *Attr {
 	vv := make([]cty.Value, len(vs))
diff --git a/sql/internal/specutil/convert.go b/sql/internal/specutil/convert.go
index 0fcbd8c6..17c433eb 100644
--- a/sql/internal/specutil/convert.go
+++ b/sql/internal/specutil/convert.go
@@ -47,12 +47,22 @@ type (
 		Views        []*sqlspec.View
 		Materialized []*sqlspec.View
 	}
+
 	// ScanFuncs represents a set of scan functions
 	// used to convert the HCL document to the Realm.
 	ScanFuncs struct {
 		Table ConvertTableFunc
 		View  ConvertViewFunc
 	}
+
+	// Funcs represents a set of spec functions
+	// used to convert the Realm to an HCL document.
+	Funcs struct {
+		Table TableSpecFunc
+		View  ViewSpecFunc
+		Func  func(*schema.Func) (*sqlspec.Func, error)
+		Proc  func(*schema.Proc) (*sqlspec.Func, error)
+	}
 )
 
 const (
@@ -441,27 +451,27 @@ func linkForeignKeys(tbl *schema.Table, fks []*sqlspec.ForeignKey) error {
 }
 
 // FromSchema converts a schema.Schema into sqlspec.Schema and []sqlspec.Table.
-func FromSchema(s *schema.Schema, specT TableSpecFunc, specV ViewSpecFunc) (*SchemaSpec, error) {
-	var (
-		spec = &sqlspec.Schema{
+func FromSchema(s *schema.Schema, funcs Funcs) (*SchemaSpec, error) {
+	spec := &SchemaSpec{
+		Schema: &sqlspec.Schema{
 			Name: s.Name,
-		}
-		views  = make([]*sqlspec.View, 0, len(s.Views))
-		mviews = make([]*sqlspec.View, 0, len(s.Views))
-		tables = make([]*sqlspec.Table, 0, len(s.Tables))
-	)
+		},
+		Tables:       make([]*sqlspec.Table, 0, len(s.Tables)),
+		Views:        make([]*sqlspec.View, 0, len(s.Views)),
+		Materialized: make([]*sqlspec.View, 0, len(s.Views)),
+	}
 	for _, t := range s.Tables {
-		table, err := specT(t)
+		table, err := funcs.Table(t)
 		if err != nil {
 			return nil, err
 		}
 		if s.Name != "" {
 			table.Schema = SchemaRef(s.Name)
 		}
-		tables = append(tables, table)
+		spec.Tables = append(spec.Tables, table)
 	}
 	for _, v := range s.Views {
-		view, err := specV(v)
+		view, err := funcs.View(v)
 		if err != nil {
 			return nil, err
 		}
@@ -469,18 +479,37 @@ func FromSchema(s *schema.Schema, specT TableSpecFunc, specV ViewSpecFunc) (*Sch
 			view.Schema = SchemaRef(s.Name)
 		}
 		if v.Materialized() {
-			mviews = append(mviews, view)
+			spec.Materialized = append(spec.Materialized, view)
 		} else {
-			views = append(views, view)
+			spec.Views = append(spec.Views, view)
 		}
 	}
-	convertCommentFromSchema(s.Attrs, &spec.Extra.Attrs)
-	return &SchemaSpec{
-		Schema:       spec,
-		Tables:       tables,
-		Views:        views,
-		Materialized: mviews,
-	}, nil
+	if funcs.Func != nil {
+		for _, f := range s.Funcs {
+			fn, err := funcs.Func(f)
+			if err != nil {
+				return nil, err
+			}
+			if s.Name != "" {
+				fn.Schema = SchemaRef(s.Name)
+			}
+			spec.Funcs = append(spec.Funcs, fn)
+		}
+	}
+	if funcs.Proc != nil {
+		for _, p := range s.Procs {
+			pr, err := funcs.Proc(p)
+			if err != nil {
+				return nil, err
+			}
+			if s.Name != "" {
+				pr.Schema = SchemaRef(s.Name)
+			}
+			spec.Procs = append(spec.Procs, pr)
+		}
+	}
+	convertCommentFromSchema(s.Attrs, &spec.Schema.Extra.Attrs)
+	return spec, nil
 }
 
 // FromTable converts a schema.Table to a sqlspec.Table.
diff --git a/sql/internal/specutil/spec.go b/sql/internal/specutil/spec.go
index 8b5ccc3f..6967fd1c 100644
--- a/sql/internal/specutil/spec.go
+++ b/sql/internal/specutil/spec.go
@@ -27,12 +27,16 @@ type (
 		Schema       *sqlspec.Schema
 		Tables       []*sqlspec.Table
 		Views        []*sqlspec.View
+		Funcs        []*sqlspec.Func
+		Procs        []*sqlspec.Func
 		Materialized []*sqlspec.View
 	}
 	doc struct {
 		Tables       []*sqlspec.Table  `spec:"table"`
 		Views        []*sqlspec.View   `spec:"view"`
 		Materialized []*sqlspec.View   `spec:"materialized"`
+		Funcs        []*sqlspec.Func   `spec:"function"`
+		Procs        []*sqlspec.Func   `spec:"procedure"`
 		Schemas      []*sqlspec.Schema `spec:"schema"`
 	}
 )
diff --git a/sql/internal/sqlx/sqlx.go b/sql/internal/sqlx/sqlx.go
index b516cd66..bfa073fe 100644
--- a/sql/internal/sqlx/sqlx.go
+++ b/sql/internal/sqlx/sqlx.go
@@ -224,7 +224,7 @@ func ValuesEqual(v1, v2 []string) bool {
 // ModeInspectSchema returns the InspectMode or its default.
 func ModeInspectSchema(o *schema.InspectOptions) schema.InspectMode {
 	if o == nil || o.Mode == 0 {
-		return schema.InspectSchemas | schema.InspectTables | schema.InspectViews
+		return schema.InspectSchemas | schema.InspectTables | schema.InspectViews | schema.InspectFuncs
 	}
 	return o.Mode
 }
@@ -232,7 +232,7 @@ func ModeInspectSchema(o *schema.InspectOptions) schema.InspectMode {
 // ModeInspectRealm returns the InspectMode or its default.
 func ModeInspectRealm(o *schema.InspectRealmOption) schema.InspectMode {
 	if o == nil || o.Mode == 0 {
-		return schema.InspectSchemas | schema.InspectTables | schema.InspectViews
+		return schema.InspectSchemas | schema.InspectTables | schema.InspectViews | schema.InspectFuncs
 	}
 	return o.Mode
 }
diff --git a/sql/mysql/sqlspec.go b/sql/mysql/sqlspec.go
index 9ee5dc95..b3d58f90 100644
--- a/sql/mysql/sqlspec.go
+++ b/sql/mysql/sqlspec.go
@@ -276,7 +276,10 @@ func convertColumnType(spec *sqlspec.Column) (schema.Type, error) {
 
 // schemaSpec converts from a concrete MySQL schema to Atlas specification.
 func schemaSpec(s *schema.Schema) (*specutil.SchemaSpec, error) {
-	spec, err := specutil.FromSchema(s, tableSpec, viewSpec)
+	spec, err := specutil.FromSchema(s, specutil.Funcs{
+		Table: tableSpec,
+		View:  viewSpec,
+	})
 	if err != nil {
 		return nil, err
 	}
diff --git a/sql/postgres/convert.go b/sql/postgres/convert.go
index ade320f7..6ac49020 100644
--- a/sql/postgres/convert.go
+++ b/sql/postgres/convert.go
@@ -164,6 +164,8 @@ func FormatType(t schema.Type) (string, error) {
 		f = strings.ToLower(t.T)
 	case *XMLType:
 		f = strings.ToLower(t.T)
+	case *PseudoType:
+		f = strings.ToLower(t.T)
 	case *schema.UnsupportedType:
 		return "", fmt.Errorf("postgres: unsupported type: %q", t.T)
 	default:
@@ -280,6 +282,9 @@ func columnType(c *columnDesc) (schema.Type, error) {
 		typ = &OIDType{T: t}
 	case TypeUserDefined:
 		typ = &UserDefinedType{T: c.fmtype}
+	case typeAny, typeAnyElement, typeAnyArray, typeAnyNonArray, typeAnyEnum, typeInternal,
+		typeRecord, typeTrigger, typeVoid, typeUnknown:
+		typ = &PseudoType{T: t}
 	default:
 		typ = &schema.UnsupportedType{T: t}
 	}
diff --git a/sql/postgres/driver.go b/sql/postgres/driver.go
index ef78e730..bbdfa2cf 100644
--- a/sql/postgres/driver.go
+++ b/sql/postgres/driver.go
@@ -435,6 +435,18 @@ const (
 	typeRegProcedure  = "regprocedure"
 	typeRegRole       = "regrole"
 	typeRegType       = "regtype"
+
+	// PostgreSQL of supported pseudo-types.
+	typeAny         = "any"
+	typeAnyElement  = "anyelement"
+	typeAnyArray    = "anyarray"
+	typeAnyNonArray = "anynonarray"
+	typeAnyEnum     = "anyenum"
+	typeInternal    = "internal"
+	typeRecord      = "record"
+	typeTrigger     = "trigger"
+	typeVoid        = "void"
+	typeUnknown     = "unknown"
 )
 
 // List of supported index types.
diff --git a/sql/postgres/driver_oss.go b/sql/postgres/driver_oss.go
index 4534c13e..dee36099 100644
--- a/sql/postgres/driver_oss.go
+++ b/sql/postgres/driver_oss.go
@@ -10,15 +10,26 @@ import (
 	"context"
 
 	"ariga.io/atlas/schemahcl"
+	"ariga.io/atlas/sql/internal/specutil"
 	"ariga.io/atlas/sql/schema"
 )
 
-var specOptions []schemahcl.Option
+var (
+	specOptions []schemahcl.Option
+	specFuncs   = specutil.Funcs{
+		Table: tableSpec,
+		View:  viewSpec,
+	}
+)
 
 func (*inspect) inspectViews(context.Context, *schema.Realm, *schema.InspectOptions) error {
 	return nil // unimplemented.
 }
 
+func (i *inspect) inspectFuncs(context.Context, *schema.Realm, *schema.InspectOptions) error {
+	return nil // unimplemented.
+}
+
 func (*state) addView(*schema.AddView) error {
 	return nil // unimplemented.
 }
diff --git a/sql/postgres/inspect.go b/sql/postgres/inspect.go
index 27f431ca..a38580f5 100644
--- a/sql/postgres/inspect.go
+++ b/sql/postgres/inspect.go
@@ -48,6 +48,11 @@ func (i *inspect) InspectRealm(ctx context.Context, opts *schema.InspectRealmOpt
 				return nil, err
 			}
 		}
+		if mode.Is(schema.InspectFuncs) {
+			if err := i.inspectFuncs(ctx, r, nil); err != nil {
+				return nil, err
+			}
+		}
 		if err := i.inspectEnums(ctx, r); err != nil {
 			return nil, err
 		}
@@ -83,6 +88,11 @@ func (i *inspect) InspectSchema(ctx context.Context, name string, opts *schema.I
 			return nil, err
 		}
 	}
+	if sqlx.ModeInspectSchema(opts).Is(schema.InspectFuncs) {
+		if err := i.inspectFuncs(ctx, r, opts); err != nil {
+			return nil, err
+		}
+	}
 	if err := i.inspectEnums(ctx, r); err != nil {
 		return nil, err
 	}
@@ -220,7 +230,7 @@ func (i *inspect) addColumn(s *schema.Schema, rows *sql.Rows) (err error) {
 		timePrecision: &timeprecision.Int64,
 	})
 	if defaults.Valid {
-		defaultExpr(c, defaults.String)
+		columnDefault(c, defaults.String)
 	}
 	if identity.String == "YES" {
 		c.Attrs = append(c.Attrs, &Identity{
@@ -677,7 +687,7 @@ func nArgs(start, n int) string {
 // nextval('<optional (quoted) schema>.<sequence name>'::regclass).
 var reNextval = regexp.MustCompile(`(?i) *nextval\('(?:"?[\w$]+"?\.)?"?([\w$]+_[\w$]+_seq)"?'(?:::regclass)*\) *$`)
 
-func defaultExpr(c *schema.Column, s string) {
+func columnDefault(c *schema.Column, s string) {
 	switch m := reNextval.FindStringSubmatch(s); {
 	// The definition of "<column> <serial type>" is equivalent to specifying:
 	// "<column> <int type> NOT NULL DEFAULT nextval('<table>_<column>_seq')".
@@ -691,26 +701,33 @@ func defaultExpr(c *schema.Column, s string) {
 		st.SetType(tt)
 		c.Type.Raw = st.T
 		c.Type.Type = st
+	default:
+		c.Default = defaultExpr(c.Type.Type, s)
+	}
+}
+
+func defaultExpr(t schema.Type, s string) schema.Expr {
+	switch {
 	case sqlx.IsLiteralBool(s), sqlx.IsLiteralNumber(s), sqlx.IsQuoted(s, '\''):
-		c.Default = &schema.Literal{V: s}
+		return &schema.Literal{V: s}
 	default:
 		var x schema.Expr = &schema.RawExpr{X: s}
 		// Try casting or fallback to raw expressions (e.g. column text[] has the default of '{}':text[]).
-		if v, ok := canConvert(c.Type, s); ok {
+		if v, ok := canConvert(t, s); ok {
 			x = &schema.Literal{V: v}
 		}
-		c.Default = x
+		return x
 	}
 }
 
-func canConvert(t *schema.ColumnType, x string) (string, bool) {
+func canConvert(t schema.Type, x string) (string, bool) {
 	i := strings.LastIndex(x, "::")
 	if i == -1 || !sqlx.IsQuoted(x[:i], '\'') {
 		return "", false
 	}
 	q := x[0:i]
 	x = x[1 : i-1]
-	switch t.Type.(type) {
+	switch t.(type) {
 	case *enumType:
 		return q, true
 	case *schema.BoolType:
@@ -734,6 +751,13 @@ type (
 		T string
 	}
 
+	// PseudoType defines a non-column pseudo-type, such as function arguments and return types.
+	// https://www.postgresql.org/docs/current/datatype-pseudo.html
+	PseudoType struct {
+		schema.Type
+		T string // e.g., void, any, cstring, etc.
+	}
+
 	// enumType represents an enum type. It serves aa intermediate representation of a Postgres enum type,
 	// to temporary save TypeID and TypeName of an enum column until the enum values can be extracted.
 	enumType struct {
diff --git a/sql/postgres/sqlspec.go b/sql/postgres/sqlspec.go
index 2cb7cf20..1be2197d 100644
--- a/sql/postgres/sqlspec.go
+++ b/sql/postgres/sqlspec.go
@@ -27,6 +27,8 @@ type (
 		Views        []*sqlspec.View   `spec:"view"`
 		Materialized []*sqlspec.View   `spec:"materialized"`
 		Enums        []*Enum           `spec:"enum"`
+		Funcs        []*sqlspec.Func   `spec:"function"`
+		Procs        []*sqlspec.Func   `spec:"procedure"`
 		Schemas      []*sqlspec.Schema `spec:"schema"`
 	}
 	// Enum holds a specification for an enum, that can be referenced as a column type.
@@ -45,6 +47,8 @@ func (d *doc) merge(d1 *doc) {
 	d.Views = append(d.Views, d1.Views...)
 	d.Materialized = append(d.Materialized, d1.Materialized...)
 	d.Enums = append(d.Enums, d1.Enums...)
+	d.Funcs = append(d.Funcs, d1.Funcs...)
+	d.Procs = append(d.Procs, d1.Procs...)
 	d.Schemas = append(d.Schemas, d1.Schemas...)
 }
 
@@ -140,6 +144,12 @@ func MarshalSpec(v any, marshaler schemahcl.Marshaler) ([]byte, error) {
 		if err := specutil.QualifyObjects(d.Enums); err != nil {
 			return nil, err
 		}
+		if err := specutil.QualifyObjects(d.Funcs); err != nil {
+			return nil, err
+		}
+		if err := specutil.QualifyObjects(d.Procs); err != nil {
+			return nil, err
+		}
 		if err := specutil.QualifyReferences(d.Tables, s); err != nil {
 			return nil, err
 		}
@@ -573,7 +583,7 @@ func enumRef(n string) *schemahcl.Ref {
 
 // schemaSpec converts from a concrete Postgres schema to Atlas specification.
 func schemaSpec(s *schema.Schema) (*doc, error) {
-	spec, err := specutil.FromSchema(s, tableSpec, viewSpec)
+	spec, err := specutil.FromSchema(s, specFuncs)
 	if err != nil {
 		return nil, err
 	}
@@ -581,6 +591,8 @@ func schemaSpec(s *schema.Schema) (*doc, error) {
 		Tables:       spec.Tables,
 		Views:        spec.Views,
 		Materialized: spec.Materialized,
+		Funcs:        spec.Funcs,
+		Procs:        spec.Procs,
 		Schemas:      []*sqlspec.Schema{spec.Schema},
 		Enums:        make([]*Enum, 0, len(s.Objects)),
 	}
@@ -812,11 +824,13 @@ var TypeRegistry = schemahcl.NewRegistry(
 		schemahcl.NewTypeSpec("hstore"),
 		schemahcl.NewTypeSpec("sql", schemahcl.WithAttributes(&schemahcl.TypeAttr{Name: "def", Required: true, Kind: reflect.String})),
 	),
-	// PostgreSQL internal and special types.
+	// PostgreSQL internal, pseudo, and special types.
 	schemahcl.WithSpecs(func() (specs []*schemahcl.TypeSpec) {
 		for _, t := range []string{
 			typeOID, typeRegClass, typeRegCollation, typeRegConfig, typeRegDictionary, typeRegNamespace,
 			typeName, typeRegOper, typeRegOperator, typeRegProc, typeRegProcedure, typeRegRole, typeRegType,
+			typeAny, typeAnyElement, typeAnyArray, typeAnyNonArray, typeAnyEnum, typeInternal, typeRecord,
+			typeTrigger, typeVoid, typeUnknown,
 		} {
 			specs = append(specs, schemahcl.NewTypeSpec(t))
 		}
diff --git a/sql/schema/inspect.go b/sql/schema/inspect.go
index 61df2c01..fe2cce17 100644
--- a/sql/schema/inspect.go
+++ b/sql/schema/inspect.go
@@ -46,6 +46,9 @@ const (
 
 	// InspectViews enables schema views inspection.
 	InspectViews
+
+	// InspectFuncs enables schema functions / procedures inspection.
+	InspectFuncs
 )
 
 // Is reports whether the given mode is enabled.
diff --git a/sql/schema/schema.go b/sql/schema/schema.go
index c8563e98..2b6adc0b 100644
--- a/sql/schema/schema.go
+++ b/sql/schema/schema.go
@@ -18,6 +18,8 @@ type (
 		Realm   *Realm
 		Tables  []*Table
 		Views   []*View
+		Funcs   []*Func
+		Procs   []*Proc
 		Attrs   []Attr   // Attrs and options.
 		Objects []Object // Driver specific objects.
 	}
@@ -105,6 +107,46 @@ type (
 		OnUpdate   ReferenceOption
 		OnDelete   ReferenceOption
 	}
+
+	// Func represents a function definition.
+	Func struct {
+		Name   string
+		Schema *Schema
+		Args   []*FuncArg
+		Ret    Type
+		Body   string // Function body only.
+		Lang   string // Language (e.g. SQL, PL/pgSQL, etc.).
+		Attrs  []Attr // Extra driver specific attributes.
+	}
+
+	// Proc represents a procedure definition.
+	Proc struct {
+		Name   string
+		Schema *Schema
+		Args   []*FuncArg
+		Body   string // Function body only.
+		Lang   string // Language (e.g. SQL, PL/pgSQL, etc.).
+		Attrs  []Attr // Extra driver specific attributes.
+	}
+
+	// A FuncArg represents a single function argument.
+	FuncArg struct {
+		Name    string      // Optional name.
+		Type    Type        // Argument type.
+		Default Expr        // Default value.
+		Mode    FuncArgMode // Argument mode.
+	}
+
+	// FuncArgMode represents a function argument mode.
+	FuncArgMode string
+)
+
+// List of supported function argument modes.
+const (
+	FuncArgModeIn       FuncArgMode = "IN"
+	FuncArgModeOut      FuncArgMode = "OUT"
+	FuncArgModeInOut    FuncArgMode = "INOUT"
+	FuncArgModeVariadic FuncArgMode = "VARIADIC"
 )
 
 // Schema returns the first schema that matched the given name.
diff --git a/sql/sqlite/sqlspec.go b/sql/sqlite/sqlspec.go
index 10df474c..747f2fe3 100644
--- a/sql/sqlite/sqlspec.go
+++ b/sql/sqlite/sqlspec.go
@@ -158,7 +158,10 @@ func convertColumnType(spec *sqlspec.Column) (schema.Type, error) {
 
 // schemaSpec converts from a concrete SQLite schema to Atlas specification.
 func schemaSpec(s *schema.Schema) (*specutil.SchemaSpec, error) {
-	return specutil.FromSchema(s, tableSpec, viewSpec)
+	return specutil.FromSchema(s, specutil.Funcs{
+		Table: tableSpec,
+		View:  viewSpec,
+	})
 }
 
 // tableSpec converts from a concrete SQLite sqlspec.Table to a schema.Table.
diff --git a/sql/sqlspec/sqlspec.go b/sql/sqlspec/sqlspec.go
index e163d279..6c2e400b 100644
--- a/sql/sqlspec/sqlspec.go
+++ b/sql/sqlspec/sqlspec.go
@@ -93,8 +93,27 @@ type (
 		schemahcl.DefaultExtension
 	}
 
-	// Type represents a database agnostic column type.
-	Type string
+	// Func holds the specification for a function.
+	Func struct {
+		Name      string         `spec:",name"`
+		Qualifier string         `spec:",qualifier"`
+		Schema    *schemahcl.Ref `spec:"schema"`
+		Args      []*FuncArg     `spec:"arg"`
+		Lang      cty.Value      `spec:"lang"`
+		// The definition and the return type are appended as additional
+		// attribute by the spec creator to marshal it after the arguments.
+		schemahcl.DefaultExtension
+	}
+
+	// FuncArg holds the specification for a function argument.
+	FuncArg struct {
+		Name    string          `spec:",name"`
+		Type    *schemahcl.Type `spec:"type"`
+		Default cty.Value       `spec:"default"`
+		// Optional attributes such as mode are added by the driver,
+		// as their definition can be either a string or an enum (ref).
+		schemahcl.DefaultExtension
+	}
 )
 
 // Label returns the defaults label used for the table resource.
@@ -112,7 +131,7 @@ func (t *Table) SchemaRef() *schemahcl.Ref { return t.Schema }
 // Label returns the defaults label used for the view resource.
 func (v *View) Label() string { return v.Name }
 
-// QualifierLabel returns the qualifier label used for the table resource, if any.
+// QualifierLabel returns the qualifier label used for the view resource, if any.
 func (v *View) QualifierLabel() string { return v.Qualifier }
 
 // SetQualifier sets the qualifier label used for the view resource.
@@ -121,9 +140,23 @@ func (v *View) SetQualifier(q string) { v.Qualifier = q }
 // SchemaRef returns the schema reference for the view.
 func (v *View) SchemaRef() *schemahcl.Ref { return v.Schema }
 
+// Label returns the defaults label used for the function resource.
+func (f *Func) Label() string { return f.Name }
+
+// QualifierLabel returns the qualifier label used for the function resource, if any.
+func (f *Func) QualifierLabel() string { return f.Qualifier }
+
+// SetQualifier sets the qualifier label used for the function resource.
+func (f *Func) SetQualifier(q string) { f.Qualifier = q }
+
+// SchemaRef returns the schema reference for the function.
+func (f *Func) SchemaRef() *schemahcl.Ref { return f.Schema }
+
 func init() {
 	schemahcl.Register("view", &View{})
 	schemahcl.Register("materialized", &View{})
 	schemahcl.Register("table", &Table{})
+	schemahcl.Register("function", &Func{})
+	schemahcl.Register("procedure", &Func{})
 	schemahcl.Register("schema", &Schema{})
 }


Test patch:
diff --git a/sql/internal/specutil/convert_test.go b/sql/internal/specutil/convert_test.go
index 6879a5a3..72528f14 100644
--- a/sql/internal/specutil/convert_test.go
+++ b/sql/internal/specutil/convert_test.go
@@ -22,15 +22,14 @@ func TestFromSpec_SchemaName(t *testing.T) {
 		},
 	}
 	sc.Tables[0].Schema = sc
-	spec, err := FromSchema(
-		sc,
-		func(*schema.Table) (*sqlspec.Table, error) {
+	spec, err := FromSchema(sc, Funcs{
+		Table: func(*schema.Table) (*sqlspec.Table, error) {
 			return &sqlspec.Table{}, nil
 		},
-		func(*schema.View) (*sqlspec.View, error) {
+		View: func(*schema.View) (*sqlspec.View, error) {
 			return &sqlspec.View{}, nil
 		},
-	)
+	})
 	require.NoError(t, err)
 	require.Equal(t, sc.Name, spec.Schema.Name)
 	require.Equal(t, "$schema."+sc.Name, spec.Tables[0].Schema.V)