Skip to content

Commit ec08867

Browse files
dump247cyrilgdn
andauthored
Add function resource (#200)
* Add function resource * Add documentation for postgresql_function resource Fix missing quotes in physical_replication_slot doc. * Add postgresql version check Older versions of postgresql do not support the necessary syntax in CREATE FUNCTION. * Fix environment setup script file reference in readme * Added tests Fixed some issues that were exposed by the tests * Document default for drop_cascade * Removed unnecessary transactions Co-authored-by: Cyril Gaudin <cyril.gaudin@gmail.com>
1 parent f0d493d commit ec08867

8 files changed

+613
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ In order to manually run some Acceptance test locally, run the following command
7171
make testacc_setup
7272

7373
# Load the needed environment variables for the tests
74-
source tests/env.sh
74+
source tests/switch_superuser.sh
7575

7676
# Run the test(s) that you're working on as often as you want
7777
TF_LOG=INFO go test -v ./postgresql -run ^TestAccPostgresqlRole_Basic$

postgresql/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const (
3434
featurePrivilegesOnSchemas
3535
featureForceDropDatabase
3636
featurePid
37+
featureFunction
3738
)
3839

3940
var (
@@ -86,6 +87,9 @@ var (
8687
// Column procpid was replaced by pid in pg_stat_activity
8788
// for Postgresql >= 9.2 and above
8889
featurePid: semver.MustParseRange(">=9.2.0"),
90+
91+
// We do not support CREATE FUNCTION for Postgresql < 8.4
92+
featureFunction: semver.MustParseRange(">=8.4.0"),
8993
}
9094
)
9195

postgresql/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ func Provider() *schema.Provider {
164164
"postgresql_physical_replication_slot": resourcePostgreSQLPhysicalReplicationSlot(),
165165
"postgresql_schema": resourcePostgreSQLSchema(),
166166
"postgresql_role": resourcePostgreSQLRole(),
167+
"postgresql_function": resourcePostgreSQLFunction(),
167168
},
168169

169170
ConfigureFunc: providerConfigure,
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
package postgresql
2+
3+
import (
4+
"bytes"
5+
"database/sql"
6+
"fmt"
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
8+
"github.com/lib/pq"
9+
"log"
10+
)
11+
12+
const (
13+
funcNameAttr = "name"
14+
funcSchemaAttr = "schema"
15+
funcBodyAttr = "body"
16+
funcArgAttr = "arg"
17+
funcReturnsAttr = "returns"
18+
funcDropCascadeAttr = "drop_cascade"
19+
20+
funcArgTypeAttr = "type"
21+
funcArgNameAttr = "name"
22+
funcArgModeAttr = "mode"
23+
funcArgDefaultAttr = "default"
24+
)
25+
26+
func resourcePostgreSQLFunction() *schema.Resource {
27+
return &schema.Resource{
28+
Create: PGResourceFunc(resourcePostgreSQLFunctionCreate),
29+
Read: PGResourceFunc(resourcePostgreSQLFunctionRead),
30+
Update: PGResourceFunc(resourcePostgreSQLFunctionUpdate),
31+
Delete: PGResourceFunc(resourcePostgreSQLFunctionDelete),
32+
Exists: PGResourceExistsFunc(resourcePostgreSQLFunctionExists),
33+
Importer: &schema.ResourceImporter{
34+
StateContext: schema.ImportStatePassthroughContext,
35+
},
36+
37+
Schema: map[string]*schema.Schema{
38+
funcSchemaAttr: {
39+
Type: schema.TypeString,
40+
Optional: true,
41+
Computed: true,
42+
ForceNew: true,
43+
Description: "Schema where the function is located. If not specified, the provider default schema is used.",
44+
},
45+
funcNameAttr: {
46+
Type: schema.TypeString,
47+
Required: true,
48+
ForceNew: true,
49+
Description: "Name of the function.",
50+
},
51+
funcArgAttr: {
52+
Type: schema.TypeList,
53+
Elem: &schema.Resource{
54+
Schema: map[string]*schema.Schema{
55+
funcArgTypeAttr: {
56+
Type: schema.TypeString,
57+
Description: "The argument type.",
58+
Required: true,
59+
ForceNew: true,
60+
},
61+
funcArgNameAttr: {
62+
Type: schema.TypeString,
63+
Description: "The argument name. The name may be required for some languages or depending on the argument mode.",
64+
Optional: true,
65+
ForceNew: true,
66+
},
67+
funcArgModeAttr: {
68+
Type: schema.TypeString,
69+
Description: "The argument mode. One of: IN, OUT, INOUT, or VARIADIC",
70+
Optional: true,
71+
Default: "IN",
72+
ForceNew: true,
73+
},
74+
funcArgDefaultAttr: {
75+
Type: schema.TypeString,
76+
Description: "An expression to be used as default value if the parameter is not specified.",
77+
Optional: true,
78+
},
79+
},
80+
},
81+
Optional: true,
82+
ForceNew: true,
83+
Description: "Function argument definitions.",
84+
},
85+
funcReturnsAttr: {
86+
Type: schema.TypeString,
87+
Optional: true,
88+
ForceNew: true,
89+
Description: "Function return type.",
90+
},
91+
funcBodyAttr: {
92+
Type: schema.TypeString,
93+
Required: true,
94+
Description: "Body of the function.",
95+
},
96+
funcDropCascadeAttr: {
97+
Type: schema.TypeBool,
98+
Description: "Automatically drop objects that depend on the function (such as operators or triggers), and in turn all objects that depend on those objects.",
99+
Optional: true,
100+
Default: false,
101+
},
102+
},
103+
}
104+
}
105+
106+
func resourcePostgreSQLFunctionCreate(db *DBConnection, d *schema.ResourceData) error {
107+
if !db.featureSupported(featureFunction) {
108+
return fmt.Errorf(
109+
"postgresql_function resource is not supported for this Postgres version (%s)",
110+
db.version,
111+
)
112+
}
113+
114+
if err := createFunction(db, d, false); err != nil {
115+
return err
116+
}
117+
118+
return resourcePostgreSQLFunctionReadImpl(db, d)
119+
}
120+
121+
func resourcePostgreSQLFunctionExists(db *DBConnection, d *schema.ResourceData) (bool, error) {
122+
if !db.featureSupported(featureFunction) {
123+
return false, fmt.Errorf(
124+
"postgresql_function resource is not supported for this Postgres version (%s)",
125+
db.version,
126+
)
127+
}
128+
129+
signature := getFunctionSignature(d)
130+
functionExists := false
131+
132+
query := fmt.Sprintf("SELECT to_regprocedure('%s') IS NOT NULL", signature)
133+
err := db.QueryRow(query).Scan(&functionExists)
134+
return functionExists, err
135+
}
136+
137+
func resourcePostgreSQLFunctionRead(db *DBConnection, d *schema.ResourceData) error {
138+
if !db.featureSupported(featureFunction) {
139+
return fmt.Errorf(
140+
"postgresql_function resource is not supported for this Postgres version (%s)",
141+
db.version,
142+
)
143+
}
144+
145+
return resourcePostgreSQLFunctionReadImpl(db, d)
146+
}
147+
148+
func resourcePostgreSQLFunctionReadImpl(db *DBConnection, d *schema.ResourceData) error {
149+
signature := getFunctionSignature(d)
150+
151+
var funcSchema, funcName string
152+
153+
query := `SELECT n.nspname, p.proname ` +
154+
`FROM pg_proc p ` +
155+
`LEFT JOIN pg_namespace n ON p.pronamespace = n.oid ` +
156+
`WHERE p.oid = to_regprocedure($1)`
157+
err := db.QueryRow(query, signature).Scan(&funcSchema, &funcName)
158+
switch {
159+
case err == sql.ErrNoRows:
160+
log.Printf("[WARN] PostgreSQL function: %s", signature)
161+
d.SetId("")
162+
return nil
163+
case err != nil:
164+
return fmt.Errorf("Error reading function: %w", err)
165+
}
166+
167+
d.Set(funcNameAttr, funcName)
168+
d.Set(funcSchemaAttr, funcSchema)
169+
d.SetId(signature)
170+
171+
return nil
172+
}
173+
174+
func resourcePostgreSQLFunctionDelete(db *DBConnection, d *schema.ResourceData) error {
175+
if !db.featureSupported(featureFunction) {
176+
return fmt.Errorf(
177+
"postgresql_function resource is not supported for this Postgres version (%s)",
178+
db.version,
179+
)
180+
}
181+
182+
signature := getFunctionSignature(d)
183+
184+
dropMode := "RESTRICT"
185+
if v, ok := d.GetOk(funcDropCascadeAttr); ok && v.(bool) {
186+
dropMode = "CASCADE"
187+
}
188+
189+
sql := fmt.Sprintf("DROP FUNCTION IF EXISTS %s %s", signature, dropMode)
190+
if _, err := db.Exec(sql); err != nil {
191+
return err
192+
}
193+
194+
d.SetId("")
195+
196+
return nil
197+
}
198+
199+
func resourcePostgreSQLFunctionUpdate(db *DBConnection, d *schema.ResourceData) error {
200+
if !db.featureSupported(featureFunction) {
201+
return fmt.Errorf(
202+
"postgresql_function resource is not supported for this Postgres version (%s)",
203+
db.version,
204+
)
205+
}
206+
207+
if err := createFunction(db, d, true); err != nil {
208+
return err
209+
}
210+
211+
return resourcePostgreSQLFunctionReadImpl(db, d)
212+
}
213+
214+
func createFunction(db *DBConnection, d *schema.ResourceData, replace bool) error {
215+
b := bytes.NewBufferString("CREATE ")
216+
217+
if replace {
218+
b.WriteString(" OR REPLACE ")
219+
}
220+
221+
b.WriteString("FUNCTION ")
222+
223+
if v, ok := d.GetOk(funcSchemaAttr); ok {
224+
fmt.Fprint(b, pq.QuoteIdentifier(v.(string)), ".")
225+
}
226+
227+
fmt.Fprint(b, pq.QuoteIdentifier(d.Get(funcNameAttr).(string)), " (")
228+
229+
if args, ok := d.GetOk(funcArgAttr); ok {
230+
args := args.([]interface{})
231+
232+
for i, arg := range args {
233+
arg := arg.(map[string]interface{})
234+
235+
if i > 0 {
236+
b.WriteRune(',')
237+
}
238+
239+
b.WriteString("\n ")
240+
241+
if v, ok := arg[funcArgModeAttr]; ok {
242+
fmt.Fprint(b, v.(string), " ")
243+
}
244+
245+
if v, ok := arg[funcArgNameAttr]; ok {
246+
fmt.Fprint(b, v.(string), " ")
247+
}
248+
249+
b.WriteString(arg[funcArgTypeAttr].(string))
250+
251+
if v, ok := arg[funcArgDefaultAttr]; ok {
252+
v := v.(string)
253+
254+
if len(v) > 0 {
255+
fmt.Fprint(b, " DEFAULT ", v)
256+
}
257+
}
258+
}
259+
260+
if len(args) > 0 {
261+
b.WriteRune('\n')
262+
}
263+
}
264+
265+
b.WriteString(")")
266+
267+
if v, ok := d.GetOk(funcReturnsAttr); ok {
268+
fmt.Fprint(b, " RETURNS ", v.(string))
269+
}
270+
271+
fmt.Fprint(b, "\n", d.Get(funcBodyAttr).(string))
272+
273+
sql := b.String()
274+
if _, err := db.Exec(sql); err != nil {
275+
return err
276+
}
277+
278+
return nil
279+
}
280+
281+
func getFunctionSignature(d *schema.ResourceData) string {
282+
b := bytes.NewBufferString("")
283+
284+
if v, ok := d.GetOk(funcSchemaAttr); ok {
285+
fmt.Fprint(b, pq.QuoteIdentifier(v.(string)), ".")
286+
}
287+
288+
fmt.Fprint(b, pq.QuoteIdentifier(d.Get(funcNameAttr).(string)), "(")
289+
290+
if args, ok := d.GetOk(funcArgAttr); ok {
291+
argCount := 0
292+
293+
for _, arg := range args.([]interface{}) {
294+
arg := arg.(map[string]interface{})
295+
296+
mode := "IN"
297+
298+
if v, ok := arg[funcArgModeAttr]; ok {
299+
mode = v.(string)
300+
}
301+
302+
if mode != "OUT" {
303+
if argCount > 0 {
304+
b.WriteRune(',')
305+
}
306+
307+
b.WriteString(arg[funcArgTypeAttr].(string))
308+
argCount += 1
309+
}
310+
}
311+
}
312+
313+
b.WriteRune(')')
314+
315+
return b.String()
316+
}

0 commit comments

Comments
 (0)