Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
allow define a search_path on the database
Browse files Browse the repository at this point in the history
NitriKx committed Feb 2, 2024
1 parent a961e75 commit 5ebd300
Showing 5 changed files with 156 additions and 23 deletions.
17 changes: 17 additions & 0 deletions postgresql/helpers.go
Original file line number Diff line number Diff line change
@@ -606,3 +606,20 @@ func findStringSubmatchMap(expression string, text string) map[string]string {
func defaultDiffSuppressFunc(k, old, new string, d *schema.ResourceData) bool {
return old == new
}

// readSearchPath searches for a search_path entry in the rolconfig array.
// In case no such value is present, it returns nil.
func readSearchPath(roleConfig pq.ByteaArray) []string {
searchPathPrefix := "search_path"
for _, v := range roleConfig {
config := string(v)
if strings.HasPrefix(config, searchPathPrefix) {
var result = strings.Split(strings.TrimPrefix(config, searchPathPrefix+"="), ", ")
for i := range result {
result[i] = strings.Trim(result[i], `"`)
}
return result
}
}
return nil
}
72 changes: 72 additions & 0 deletions postgresql/resource_postgresql_database.go
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ const (
dbOwnerAttr = "owner"
dbTablespaceAttr = "tablespace_name"
dbTemplateAttr = "template"
dbSearchPathAttr = "search_path"
)

func resourcePostgreSQLDatabase() *schema.Resource {
@@ -102,6 +103,13 @@ func resourcePostgreSQLDatabase() *schema.Resource {
Computed: true,
Description: "If true, then this database can be cloned by any user with CREATEDB privileges",
},
dbSearchPathAttr: {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
MinItems: 0,
Description: "Sets the database's search path",
},
},
}
}
@@ -224,6 +232,10 @@ func createDatabase(db *DBConnection, d *schema.ResourceData) error {
return fmt.Errorf("Error creating database %q: %w", dbName, err)
}

if err := alterDBSearchPath(db, d); err != nil {
return err
}

// Set err outside of the return so that the deferred revoke can override err
// if necessary.
return err
@@ -350,13 +362,20 @@ func resourcePostgreSQLDatabaseReadImpl(db *DBConnection, d *schema.ResourceData
return fmt.Errorf("Error reading database: %w", err)
}

var dbRoleSettings pq.ByteaArray
dbRoleSettings, err = getDBRoleSettings(db, dbId)
if err != nil {
return fmt.Errorf("Error reading database role settings: %w", err)
}

d.Set(dbNameAttr, dbName)
d.Set(dbOwnerAttr, ownerName)
d.Set(dbEncodingAttr, dbEncoding)
d.Set(dbCollationAttr, dbCollation)
d.Set(dbCTypeAttr, dbCType)
d.Set(dbTablespaceAttr, dbTablespaceName)
d.Set(dbConnLimitAttr, dbConnLimit)
d.Set(dbSearchPathAttr, readSearchPath(dbRoleSettings))
dbTemplate := d.Get(dbTemplateAttr).(string)
if dbTemplate == "" {
dbTemplate = "template0"
@@ -415,6 +434,10 @@ func resourcePostgreSQLDatabaseUpdate(db *DBConnection, d *schema.ResourceData)

// Empty values: ALTER DATABASE name RESET configuration_parameter;

if err := alterDBSearchPath(db, d); err != nil {
return err
}

return resourcePostgreSQLDatabaseReadImpl(db, d)
}

@@ -543,6 +566,39 @@ func setDBIsTemplate(db *DBConnection, d *schema.ResourceData) error {
return nil
}

func alterDBSearchPath(db *DBConnection, d *schema.ResourceData) error {
dbName := d.Get(dbNameAttr).(string)
searchPathInterface := d.Get(dbSearchPathAttr).([]interface{})

var searchPathString []string
if len(searchPathInterface) > 0 {
searchPathString = make([]string, len(searchPathInterface))
for i, searchPathPart := range searchPathInterface {
if strings.Contains(searchPathPart.(string), ", ") {
return fmt.Errorf("search_path cannot contain `, `: %v", searchPathPart)
}
searchPathString[i] = pq.QuoteIdentifier(searchPathPart.(string))
}
} else {
searchPathString = []string{"DEFAULT"}
}
searchPath := strings.Join(searchPathString[:], ", ")

log.Printf("[INFO] Altering PostgreSQL database (%q) search_path with: %s", dbName, searchPath)

query := fmt.Sprintf(
"ALTER DATABASE %s SET search_path TO %s", pq.QuoteIdentifier(dbName), searchPath,
)

log.Printf("[DEBUG] Altering PostgreSQL database (%q) search_path query: %s", dbName, query)

if _, err := db.Exec(query); err != nil {
return fmt.Errorf("could not set search_path %s for %s: %w", searchPath, dbName, err)
}

return nil
}

func doSetDBIsTemplate(db *DBConnection, dbName string, isTemplate bool) error {
if !db.featureSupported(featureDBIsTemplate) {
return fmt.Errorf("PostgreSQL client is talking with a server (%q) that does not support database IS_TEMPLATE", db.version.String())
@@ -577,3 +633,19 @@ func terminateBConnections(db *DBConnection, dbName string) error {

return nil
}

func getDBRoleSettings(db *DBConnection, dbId string) (pq.ByteaArray, error) {
var dbRoleConfigItems pq.ByteaArray
dbSQL := `SELECT setconfig FROM pg_catalog.pg_database AS d, pg_catalog.pg_db_role_setting AS drs WHERE d.datname = $1 AND d.oid = drs.setdatabase`
err := db.QueryRow(dbSQL, dbId).Scan(&dbRoleConfigItems)

switch {
case err == sql.ErrNoRows:
log.Printf("[WARN] PostgreSQL database (%q) not found", dbId)
return nil, nil
case err != nil:
return nil, fmt.Errorf("Error reading database: %w", err)
}

return dbRoleConfigItems, nil
}
70 changes: 63 additions & 7 deletions postgresql/resource_postgresql_database_test.go
Original file line number Diff line number Diff line change
@@ -4,7 +4,10 @@ import (
"database/sql"
"errors"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
@@ -20,7 +23,7 @@ func TestAccPostgresqlDatabase_Basic(t *testing.T) {
{
Config: testAccPostgreSQLDatabaseConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckPostgresqlDatabaseExists("postgresql_database.mydb"),
testAccCheckPostgresqlDatabaseExists("postgresql_database.mydb", nil),
resource.TestCheckResourceAttr(
"postgresql_database.mydb", "name", "mydb"),
resource.TestCheckResourceAttr(
@@ -81,6 +84,8 @@ func TestAccPostgresqlDatabase_Basic(t *testing.T) {
"postgresql_database.pathological_opts", "connection_limit", "0"),
resource.TestCheckResourceAttr(
"postgresql_database.pathological_opts", "is_template", "true"),
resource.TestCheckResourceAttr(
"postgresql_database.pathological_opts", "search_path.#", "0"),

resource.TestCheckResourceAttr(
"postgresql_database.pg_default_opts", "owner", "myrole"),
@@ -100,6 +105,8 @@ func TestAccPostgresqlDatabase_Basic(t *testing.T) {
"postgresql_database.pg_default_opts", "connection_limit", "0"),
resource.TestCheckResourceAttr(
"postgresql_database.pg_default_opts", "is_template", "true"),

testAccCheckPostgresqlDatabaseExists("postgresql_database.mydb_search_path", []string{"bar", "foo-with-hyphen"}),
),
},
},
@@ -115,7 +122,7 @@ func TestAccPostgresqlDatabase_DefaultOwner(t *testing.T) {
{
Config: testAccPostgreSQLDatabaseConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckPostgresqlDatabaseExists("postgresql_database.mydb_default_owner"),
testAccCheckPostgresqlDatabaseExists("postgresql_database.mydb_default_owner", nil),
resource.TestCheckResourceAttr(
"postgresql_database.mydb_default_owner", "name", "mydb_default_owner"),
resource.TestCheckResourceAttrSet(
@@ -154,10 +161,11 @@ func TestAccPostgresqlDatabase_Update(t *testing.T) {
resource postgresql_database test_db {
name = "test_db"
allow_connections = "%t"
search_path = ["searchpathInitial"]
}
`, allowConnections),
Check: resource.ComposeTestCheckFunc(
testAccCheckPostgresqlDatabaseExists("postgresql_database.test_db"),
testAccCheckPostgresqlDatabaseExists("postgresql_database.test_db", []string{"searchpathInitial"}),
resource.TestCheckResourceAttr("postgresql_database.test_db", "name", "test_db"),
resource.TestCheckResourceAttr("postgresql_database.test_db", "connection_limit", "-1"),
resource.TestCheckResourceAttr(
@@ -172,10 +180,11 @@ resource postgresql_database test_db {
name = "test_db"
connection_limit = 2
allow_connections = false
search_path = ["searchpathUpdated"]
}
`,
Check: resource.ComposeTestCheckFunc(
testAccCheckPostgresqlDatabaseExists("postgresql_database.test_db"),
testAccCheckPostgresqlDatabaseExists("postgresql_database.test_db", []string{"searchpathUpdated"}),
resource.TestCheckResourceAttr("postgresql_database.test_db", "name", "test_db"),
resource.TestCheckResourceAttr("postgresql_database.test_db", "connection_limit", "2"),
resource.TestCheckResourceAttr(
@@ -212,7 +221,7 @@ resource postgresql_database "test_db" {
{
Config: stateConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckPostgresqlDatabaseExists("postgresql_database.test_db"),
testAccCheckPostgresqlDatabaseExists("postgresql_database.test_db", nil),
resource.TestCheckResourceAttr("postgresql_database.test_db", "name", "test_db"),
resource.TestCheckResourceAttr("postgresql_database.test_db", "owner", "test_owner"),

@@ -254,7 +263,7 @@ resource postgresql_database "test_db" {
{
Config: stateConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckPostgresqlDatabaseExists("postgresql_database.test_db"),
testAccCheckPostgresqlDatabaseExists("postgresql_database.test_db", nil),
resource.TestCheckResourceAttr("postgresql_database.test_db", "name", "test_db"),
resource.TestCheckResourceAttr("postgresql_database.test_db", "owner", "test_owner"),

@@ -328,7 +337,7 @@ func testAccCheckPostgresqlDatabaseDestroy(s *terraform.State) error {
return nil
}

func testAccCheckPostgresqlDatabaseExists(n string) resource.TestCheckFunc {
func testAccCheckPostgresqlDatabaseExists(n string, searchPath []string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
@@ -350,6 +359,12 @@ func testAccCheckPostgresqlDatabaseExists(n string) resource.TestCheckFunc {
return errors.New("Db not found")
}

if searchPath != nil {
if err := checkDBSearchPath(client, rs.Primary.ID, searchPath); err != nil {
return fmt.Errorf("Error checking db search_path %s", err)
}
}

return nil
}
}
@@ -371,6 +386,41 @@ func checkDatabaseExists(client *Client, dbName string) (bool, error) {
return true, nil
}

func checkDBSearchPath(client *Client, dbName string, expectedSearchPath []string) error {
db, err := client.Connect()
if err != nil {
return err
}

var searchPathStr string
err = db.QueryRow(`
SELECT (pg_options_to_table(drs.setconfig)).option_value
FROM pg_catalog.pg_database AS d, pg_catalog.pg_db_role_setting AS drs
WHERE d.datname = $1 AND d.oid = drs.setdatabase`,
dbName,
).Scan(&searchPathStr)

// The query returns ErrNoRows if the search path hasn't been altered.
if err != nil && err == sql.ErrNoRows {
searchPathStr = ""
} else if err != nil {
return fmt.Errorf("Error reading search_path: %v", err)
}

searchPath := strings.Split(searchPathStr, ", ")
for i := range searchPath {
searchPath[i] = strings.Trim(searchPath[i], `"`)
}
sort.Strings(expectedSearchPath)
if !reflect.DeepEqual(searchPath, expectedSearchPath) {
return fmt.Errorf(
"search_path is not equal to expected value. expected %v - got %v",
expectedSearchPath, searchPath,
)
}
return nil
}

var testAccPostgreSQLDatabaseConfig = `
resource "postgresql_role" "myrole" {
name = "myrole"
@@ -440,10 +490,16 @@ resource "postgresql_database" "pg_default_opts" {
tablespace_name = "DEFAULT"
connection_limit = 0
is_template = true
search_path = []
}
resource "postgresql_database" "mydb_default_owner" {
name = "mydb_default_owner"
}
resource "postgresql_database" "mydb_search_path" {
name = "mydb_search_path"
search_path = ["bar", "foo-with-hyphen"]
}
`
16 changes: 0 additions & 16 deletions postgresql/resource_postgresql_role.go
Original file line number Diff line number Diff line change
@@ -483,22 +483,6 @@ func resourcePostgreSQLRoleReadImpl(db *DBConnection, d *schema.ResourceData) er
return nil
}

// readSearchPath searches for a search_path entry in the rolconfig array.
// In case no such value is present, it returns nil.
func readSearchPath(roleConfig pq.ByteaArray) []string {
for _, v := range roleConfig {
config := string(v)
if strings.HasPrefix(config, roleSearchPathAttr) {
var result = strings.Split(strings.TrimPrefix(config, roleSearchPathAttr+"="), ", ")
for i := range result {
result[i] = strings.Trim(result[i], `"`)
}
return result
}
}
return nil
}

// readIdleInTransactionSessionTimeout searches for a idle_in_transaction_session_timeout entry in the rolconfig array.
// In case no such value is present, it returns nil.
func readIdleInTransactionSessionTimeout(roleConfig pq.ByteaArray) (int, error) {
4 changes: 4 additions & 0 deletions website/docs/r/postgresql_database.html.markdown
Original file line number Diff line number Diff line change
@@ -82,6 +82,10 @@ resource "postgresql_database" "my_db" {
force the creation of a new resource as this value can only be changed when a
database is created.

* `search_path` - (Optional) Alters the search path of this new database. Note
that due to limitations in the implementation, values cannot contain the
substring `", "`.

## Import Example

`postgresql_database` supports importing resources. Supposing the following

0 comments on commit 5ebd300

Please sign in to comment.