Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/plan/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,7 @@ func normalizeSchemaNames(irData *ir.IR, fromSchema, toSchema string) {
fn.Schema = toSchema
fn.ReturnType = replaceString(fn.ReturnType)
fn.Definition = replaceString(fn.Definition)
fn.SearchPath = replaceString(fn.SearchPath)
for _, param := range fn.Parameters {
param.DataType = replaceString(param.DataType)
}
Expand Down
85 changes: 85 additions & 0 deletions internal/postgres/desired_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,91 @@ func stripSchemaQualifications(sql string, schemaName string) string {
return result
}

// replaceSchemaInSearchPath replaces the target schema name in SET search_path clauses
// within function/procedure definitions.
//
// Purpose:
// When functions or procedures have SET search_path = public, pg_temp (or similar),
// PostgreSQL uses that search_path during function body validation (for SQL-language functions)
// and execution. When applying to a temporary schema, we need to replace the target schema
// in these clauses so that table references in function bodies can be resolved.
//
// Example (when target schema is "public" and temp schema is "pgschema_tmp_xxx"):
//
// SET search_path = public, pg_temp -> SET search_path = "pgschema_tmp_xxx", pg_temp
// SET search_path TO public -> SET search_path TO "pgschema_tmp_xxx"
//
// This handles both = and TO syntax, quoted and unquoted schema names (case-insensitive),
// and preserves other schemas in the comma-separated list.
//
// Limitations:
// - Like stripSchemaQualifications and replaceSchemaInDefaultPrivileges, this function
// operates on the raw SQL string without dollar-quote awareness. A SET search_path
// inside a $$-quoted function body (e.g., dynamic SQL) would also be rewritten. In
// practice this is not an issue because such usage is extremely rare, and the round-trip
// through database inspection and normalizeSchemaNames restores the original schema name.
// - When targetSchema is "public", replacing it removes "public" from the function's
// search_path. If the function body references extension objects installed in "public"
// (e.g., citext), they may not be found. Most extension objects (uuid, jsonb, etc.) live
// in pg_catalog which is always searched, so this is rarely an issue in practice.
Comment on lines +197 to +203
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment about extension objects is misleading: uuid/jsonb are built-in types (not extension objects), and many extension-provided functions/types are commonly installed in the target schema (often public). Since this rewrite can remove the target schema from the function’s search_path, the comment’s implication that this is “rarely an issue” may not hold. Please adjust the comment (and ideally the rewrite behavior) to reflect the actual risk.

Suggested change
// inside a $$-quoted function body (e.g., dynamic SQL) would also be rewritten. In
// practice this is not an issue because such usage is extremely rare, and the round-trip
// through database inspection and normalizeSchemaNames restores the original schema name.
// - When targetSchema is "public", replacing it removes "public" from the function's
// search_path. If the function body references extension objects installed in "public"
// (e.g., citext), they may not be found. Most extension objects (uuid, jsonb, etc.) live
// in pg_catalog which is always searched, so this is rarely an issue in practice.
// inside a $$-quoted function body (e.g., dynamic SQL) would also be rewritten. While
// this usage is expected to be uncommon, it can lead to unintended rewrites; the
// round-trip through database inspection and normalizeSchemaNames will typically restore
// the original schema name but cannot guarantee that no behavioral change occurs.
// - When targetSchema is "public", replacing it removes "public" from the function's
// effective search_path within the function definition. Any unqualified references to
// objects that reside in "public" (including many extension-provided types and functions
// that are installed into the target schema) may no longer resolve after rewriting.
// Only objects in pg_catalog (for example, built-in types such as uuid and jsonb) remain
// reliably visible via the implicit search_path. Callers must ensure that functions whose
// bodies depend on objects in the target schema either qualify those references or opt
// out of search_path rewriting for those objects.

Copilot uses AI. Check for mistakes.
func replaceSchemaInSearchPath(sql string, targetSchema, tempSchema string) string {
if targetSchema == "" || tempSchema == "" {
return sql
}

replacement := fmt.Sprintf(`"%s"`, tempSchema)
Comment on lines +200 to +209
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaceSchemaInSearchPath() replaces the target schema token with only the temp schema (e.g., public -> "pgschema_tmp..."). For functions whose original SET search_path intentionally included public to resolve extension objects installed there (see the session-level SET search_path TO "temp", public added for issue #197), this rewrite can cause function creation/validation to fail because public is no longer on the function’s search_path. Consider preserving public as a fallback when the matched token is public (e.g., rewrite public -> "temp", public), or otherwise ensure extension objects remain resolvable during validation without masking temp-schema table resolution.

Suggested change
// - When targetSchema is "public", replacing it removes "public" from the function's
// search_path. If the function body references extension objects installed in "public"
// (e.g., citext), they may not be found. Most extension objects (uuid, jsonb, etc.) live
// in pg_catalog which is always searched, so this is rarely an issue in practice.
func replaceSchemaInSearchPath(sql string, targetSchema, tempSchema string) string {
if targetSchema == "" || tempSchema == "" {
return sql
}
replacement := fmt.Sprintf(`"%s"`, tempSchema)
// - When targetSchema is "public", we rewrite it to include both the temp schema and
// "public" (e.g., public -> "pgschema_tmp_xxx", public). This keeps the temp schema
// first for table resolution while preserving "public" as a fallback so extension
// objects installed there remain resolvable during function validation.
func replaceSchemaInSearchPath(sql string, targetSchema, tempSchema string) string {
if targetSchema == "" || tempSchema == "" {
return sql
}
replacement := fmt.Sprintf(`"%s"`, tempSchema)
if strings.EqualFold(targetSchema, "public") {
// Preserve "public" as a fallback so that extension objects installed in the
// public schema remain visible during function creation/validation.
replacement = fmt.Sprintf(`"%s", public`, tempSchema)
}

Copilot uses AI. Check for mistakes.

// Pattern: SET search_path = ... or SET search_path TO ...
// We match the entire SET search_path clause and replace the target schema within it.
searchPathPattern := regexp.MustCompile(`(?i)(SET\s+search_path\s*(?:=|TO)\s*)([^\n;]+)`)

// Pattern to detect trailing function body start in the captured value.
// When SET search_path and the body are on the same line, the value regex captures both.
// Handles both AS $$ (dollar-quoted) and BEGIN ATOMIC (SQL-standard, PG14+) syntax.
bodyStartPattern := regexp.MustCompile(`(?i)\s+(?:AS\s|BEGIN\s+ATOMIC\b)`)

return searchPathPattern.ReplaceAllStringFunc(sql, func(match string) string {
loc := searchPathPattern.FindStringSubmatchIndex(match)
if loc == nil {
return match
}
prefix := match[loc[2]:loc[3]]
value := match[loc[4]:loc[5]]

// Separate the search_path value from any trailing function body start
suffix := ""
if asLoc := bodyStartPattern.FindStringIndex(value); asLoc != nil {
suffix = value[asLoc[0]:]
value = value[:asLoc[0]]
}

// Tokenize the comma-separated search_path list and replace matching schemas.
// This avoids regex pitfalls with quoted identifiers (e.g., "PUBLIC" should not
// be matched by a case-insensitive unquoted pattern for "public").
tokens := strings.Split(value, ",")
for i, token := range tokens {
trimmed := strings.TrimSpace(token)
if strings.HasPrefix(trimmed, `"`) && strings.HasSuffix(trimmed, `"`) {
// Quoted identifier: case-sensitive exact match.
// "public" matches targetSchema "public", but "PUBLIC" does not
// (in PostgreSQL, quoted identifiers preserve case).
inner := trimmed[1 : len(trimmed)-1]
if inner == targetSchema {
tokens[i] = strings.Replace(token, trimmed, replacement, 1)
}
} else {
// Unquoted identifier: case-insensitive match.
// PostgreSQL folds unquoted identifiers to lowercase.
if strings.EqualFold(trimmed, targetSchema) {
tokens[i] = strings.Replace(token, trimmed, replacement, 1)
}
}
}

Comment on lines +213 to +257
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaceSchemaInSearchPath does schema token replacement case-sensitively (both the quoted and unquoted patterns). For unquoted identifiers, PostgreSQL folds case, so inputs like SET search_path = PUBLIC, pg_temp (or Public) still refer to public but won’t be rewritten and can reintroduce the same temp-schema validation failure. Consider making the schema match case-insensitive for unquoted tokens (and avoid matching inside double quotes), or parse the comma-separated search_path list and rewrite tokens by comparing normalized identifier names.

Copilot uses AI. Check for mistakes.
return prefix + strings.Join(tokens, ",") + suffix
Comment on lines +239 to +258
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic replaces the target schema token with the temp schema, which means the target schema is removed from the function’s search_path entirely. That can break creation-time validation for functions that rely on objects that exist only in the target schema (commonly extensions installed into public, e.g. gen_random_uuid() from pgcrypto). Consider rewriting to include the temp schema (e.g., insert it before the target schema) while keeping the original schemas as fallbacks, and then normalize/dedupe fn.SearchPath so generated DDL matches the original intent.

Suggested change
for i, token := range tokens {
trimmed := strings.TrimSpace(token)
if strings.HasPrefix(trimmed, `"`) && strings.HasSuffix(trimmed, `"`) {
// Quoted identifier: case-sensitive exact match.
// "public" matches targetSchema "public", but "PUBLIC" does not
// (in PostgreSQL, quoted identifiers preserve case).
inner := trimmed[1 : len(trimmed)-1]
if inner == targetSchema {
tokens[i] = strings.Replace(token, trimmed, replacement, 1)
}
} else {
// Unquoted identifier: case-insensitive match.
// PostgreSQL folds unquoted identifiers to lowercase.
if strings.EqualFold(trimmed, targetSchema) {
tokens[i] = strings.Replace(token, trimmed, replacement, 1)
}
}
}
return prefix + strings.Join(tokens, ",") + suffix
var newTokens []string
for _, token := range tokens {
trimmed := strings.TrimSpace(token)
matched := false
if strings.HasPrefix(trimmed, `"`) && strings.HasSuffix(trimmed, `"`) {
// Quoted identifier: case-sensitive exact match.
// "public" matches targetSchema "public", but "PUBLIC" does not
// (in PostgreSQL, quoted identifiers preserve case).
inner := trimmed[1 : len(trimmed)-1]
if inner == targetSchema {
newTokens = append(newTokens, replacement)
matched = true
}
} else {
// Unquoted identifier: case-insensitive match.
// PostgreSQL folds unquoted identifiers to lowercase.
if strings.EqualFold(trimmed, targetSchema) {
newTokens = append(newTokens, replacement)
matched = true
}
}
// Always keep the original token to preserve the original search_path semantics.
newTokens = append(newTokens, token)
}
return prefix + strings.Join(newTokens, ",") + suffix

Copilot uses AI. Check for mistakes.
})
Comment on lines +213 to +259
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SET search_path inside function body string literals

The searchPathPattern regex matches any SET search_path occurrence in the SQL, including those appearing as string content inside $$-quoted function bodies. For example, a PL/pgSQL function that uses dynamic SQL like EXECUTE 'SET search_path = public' would have the string literal's content incorrectly rewritten to EXECUTE 'SET search_path = "pgschema_tmp_xxx"'.

The issue arises because the regex [^\n;]+ captures up to the end of the line, and it will happily match inside $$…$$ bodies. While the common use case (function-level SET search_path = public, pg_temp as a function option) is handled correctly, any SET search_path that appears as part of a quoted string literal within a function body will also be modified, potentially corrupting dynamic SQL.

Consider guarding against this by only matching outside dollar-quoted blocks, or at minimum documenting this known limitation explicitly in the function's comment.

Comment on lines +209 to +259
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When targetSchema == "public", replacing the matching entry with the temp schema removes public from the function's search_path (e.g. public, pg_temp"pgschema_tmp...", pg_temp). This can break function creation/validation in the temp schema if the function body relies on extension objects installed in public (the session search_path explicitly keeps public as a fallback for this reason). Consider preserving public as a later fallback (while still prioritizing the temp schema), and then normalizing/de-duplicating the resulting search_path so the generated migration DDL remains unchanged.

Copilot uses AI. Check for mistakes.
}
Comment on lines +204 to +260
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No unit tests for replaceSchemaInSearchPath

The new replaceSchemaInSearchPath function has no dedicated unit tests. The only coverage comes from the integration test at testdata/diff/create_function/issue_335_search_path_rewrite/. Neither stripSchemaQualifications nor replaceSchemaInDefaultPrivileges have unit tests either, but given the regex complexity here (two-pass quoted/unquoted replacement inside a ReplaceAllStringFunc callback), a targeted table-driven test would help catch edge cases such as:

  • SET search_path TO public (the TO syntax)
  • SET search_path = "public", pg_temp (quoted target schema)
  • Multiple functions in the same SQL file
  • Schema name absent from the search path (should be a no-op)
  • Empty targetSchema or tempSchema (already guarded, but worth asserting)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


// replaceSchemaInDefaultPrivileges replaces schema names in ALTER DEFAULT PRIVILEGES statements.
// This is needed because stripSchemaQualifications only handles "schema.object" patterns,
// not "IN SCHEMA <schema>" clauses used by ALTER DEFAULT PRIVILEGES.
Expand Down
123 changes: 123 additions & 0 deletions internal/postgres/desired_state_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package postgres

import (
"testing"
)

func TestReplaceSchemaInSearchPath(t *testing.T) {
tests := []struct {
name string
sql string
targetSchema string
tempSchema string
expected string
}{
{
name: "unquoted with equals",
sql: "SET search_path = public, pg_temp",
targetSchema: "public",
tempSchema: "pgschema_tmp_20260302_000000_abcd1234",
expected: `SET search_path = "pgschema_tmp_20260302_000000_abcd1234", pg_temp`,
},
{
name: "unquoted with TO",
sql: "SET search_path TO public",
targetSchema: "public",
tempSchema: "pgschema_tmp_20260302_000000_abcd1234",
expected: `SET search_path TO "pgschema_tmp_20260302_000000_abcd1234"`,
},
{
name: "quoted target schema",
sql: `SET search_path = "public", pg_temp`,
targetSchema: "public",
tempSchema: "pgschema_tmp_20260302_000000_abcd1234",
expected: `SET search_path = "pgschema_tmp_20260302_000000_abcd1234", pg_temp`,
},
{
name: "case insensitive schema match",
sql: "SET search_path = PUBLIC, pg_temp",
targetSchema: "public",
tempSchema: "pgschema_tmp_20260302_000000_abcd1234",
expected: `SET search_path = "pgschema_tmp_20260302_000000_abcd1234", pg_temp`,
},
{
name: "mixed case schema",
sql: "SET search_path = Public, pg_temp",
targetSchema: "public",
tempSchema: "pgschema_tmp_20260302_000000_abcd1234",
expected: `SET search_path = "pgschema_tmp_20260302_000000_abcd1234", pg_temp`,
},
{
name: "schema not in search_path is no-op",
sql: "SET search_path = pg_catalog, pg_temp",
targetSchema: "public",
tempSchema: "pgschema_tmp_20260302_000000_abcd1234",
expected: "SET search_path = pg_catalog, pg_temp",
},
{
name: "multiple functions in same SQL",
sql: "CREATE FUNCTION f1() RETURNS void LANGUAGE sql SET search_path = public AS $$ SELECT 1; $$;\nCREATE FUNCTION f2() RETURNS void LANGUAGE sql SET search_path = public, pg_temp AS $$ SELECT 2; $$;",
targetSchema: "public",
tempSchema: "pgschema_tmp_xxx",
Comment on lines +57 to +61
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests cover AS $$...$$ bodies (e.g., the "multiple functions" case), but there isn't coverage for SQL-standard bodies (RETURN ... / BEGIN ATOMIC ... END) where there is no AS keyword. Adding a test for a one-line SQL-standard body combined with SET search_path would help ensure the rewrite works for both function body syntaxes.

Copilot uses AI. Check for mistakes.
expected: "CREATE FUNCTION f1() RETURNS void LANGUAGE sql SET search_path = \"pgschema_tmp_xxx\" AS $$ SELECT 1; $$;\nCREATE FUNCTION f2() RETURNS void LANGUAGE sql SET search_path = \"pgschema_tmp_xxx\", pg_temp AS $$ SELECT 2; $$;",
},
{
name: "empty target schema returns unchanged",
sql: "SET search_path = public, pg_temp",
targetSchema: "",
tempSchema: "pgschema_tmp_xxx",
expected: "SET search_path = public, pg_temp",
},
{
name: "empty temp schema returns unchanged",
sql: "SET search_path = public, pg_temp",
targetSchema: "public",
tempSchema: "",
expected: "SET search_path = public, pg_temp",
},
{
name: "no search_path in SQL is no-op",
sql: "CREATE TABLE foo (id int);",
targetSchema: "public",
tempSchema: "pgschema_tmp_xxx",
expected: "CREATE TABLE foo (id int);",
},
{
name: "non-public target schema",
sql: "SET search_path = myschema, public",
targetSchema: "myschema",
tempSchema: "pgschema_tmp_xxx",
expected: `SET search_path = "pgschema_tmp_xxx", public`,
},
{
name: "does not match partial schema names",
sql: "SET search_path = public_data, pg_temp",
targetSchema: "public",
tempSchema: "pgschema_tmp_xxx",
expected: "SET search_path = public_data, pg_temp",
},
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current unit tests don’t cover the case where the search_path list contains a quoted schema name whose case doesn’t exactly match targetSchema (e.g., SET search_path = "PUBLIC", pg_temp with targetSchema=public). Adding a test for this scenario would prevent regressions where replacements occur inside quoted identifiers and accidentally generate invalid SQL (double quotes) or change semantics.

Suggested change
},
},
{
name: "does not replace quoted schema with different case",
sql: `SET search_path = "PUBLIC", pg_temp`,
targetSchema: "public",
tempSchema: "pgschema_tmp_xxx",
expected: `SET search_path = "PUBLIC", pg_temp`,
},

Copilot uses AI. Check for mistakes.
{
name: "does not replace quoted schema with different case",
sql: `SET search_path = "PUBLIC", pg_temp`,
targetSchema: "public",
tempSchema: "pgschema_tmp_xxx",
expected: `SET search_path = "PUBLIC", pg_temp`,
},
{
name: "single-line BEGIN ATOMIC function",
sql: "CREATE FUNCTION f1() RETURNS int LANGUAGE sql SET search_path = public BEGIN ATOMIC SELECT 1; END;",
targetSchema: "public",
tempSchema: "pgschema_tmp_xxx",
expected: `CREATE FUNCTION f1() RETURNS int LANGUAGE sql SET search_path = "pgschema_tmp_xxx" BEGIN ATOMIC SELECT 1; END;`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := replaceSchemaInSearchPath(tt.sql, tt.targetSchema, tt.tempSchema)
if result != tt.expected {
t.Errorf("replaceSchemaInSearchPath() =\n%s\nwant:\n%s", result, tt.expected)
}
})
}
}
5 changes: 5 additions & 0 deletions internal/postgres/embedded.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ func (ep *EmbeddedPostgres) ApplySchema(ctx context.Context, schema string, sql
// These use "IN SCHEMA <schema>" syntax which isn't handled by stripSchemaQualifications
schemaAgnosticSQL = replaceSchemaInDefaultPrivileges(schemaAgnosticSQL, schema, ep.tempSchema)

// Replace schema names in SET search_path clauses within function/procedure definitions
// SQL-language functions are validated at creation time using the function's own search_path,
// so we need to rewrite it to point to the temporary schema (issue #335)
schemaAgnosticSQL = replaceSchemaInSearchPath(schemaAgnosticSQL, schema, ep.tempSchema)

// Execute the SQL directly
// Note: Desired state SQL should never contain operations like CREATE INDEX CONCURRENTLY
// that cannot run in transactions. Those are migration details, not state declarations.
Expand Down
5 changes: 5 additions & 0 deletions internal/postgres/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ func (ed *ExternalDatabase) ApplySchema(ctx context.Context, schema string, sql
// These use "IN SCHEMA <schema>" syntax which isn't handled by stripSchemaQualifications
schemaAgnosticSQL = replaceSchemaInDefaultPrivileges(schemaAgnosticSQL, schema, ed.tempSchema)

// Replace schema names in SET search_path clauses within function/procedure definitions
// SQL-language functions are validated at creation time using the function's own search_path,
// so we need to rewrite it to point to the temporary schema (issue #335)
schemaAgnosticSQL = replaceSchemaInSearchPath(schemaAgnosticSQL, schema, ed.tempSchema)

// Execute the SQL directly
// Note: Desired state SQL should never contain operations like CREATE INDEX CONCURRENTLY
// that cannot run in transactions. Those are migration details, not state declarations.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
CREATE TABLE IF NOT EXISTS person_accounts (
id uuid DEFAULT gen_random_uuid(),
first_name text,
last_name text,
email_address text NOT NULL,
created_at timestamptz DEFAULT now() NOT NULL,
modified_at timestamptz DEFAULT now() NOT NULL,
CONSTRAINT person_accounts_pkey PRIMARY KEY (id),
CONSTRAINT person_accounts_email_address_key UNIQUE (email_address)
);

CREATE OR REPLACE FUNCTION auth_lookup_account_by_email(
input_email text
)
RETURNS text
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
SELECT
pa.id::text AS person_account_id
FROM person_accounts pa
WHERE lower(pa.email_address) = lower(trim(input_email))
LIMIT 1;
$$;

REVOKE EXECUTE ON FUNCTION auth_lookup_account_by_email(input_email text) FROM PUBLIC;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
CREATE TABLE person_accounts (
id uuid DEFAULT gen_random_uuid(),
first_name text,
last_name text,
email_address text NOT NULL,
created_at timestamptz DEFAULT now() NOT NULL,
modified_at timestamptz DEFAULT now() NOT NULL,
CONSTRAINT person_accounts_pkey PRIMARY KEY (id),
CONSTRAINT person_accounts_email_address_key UNIQUE (email_address)
);

-- SQL-language function with SET search_path = public, pg_temp
-- that references a table. PostgreSQL validates SQL function bodies at
-- creation time using the function's own search_path, not the session's.
-- This reproduces issue #335 where the function's search_path isn't
-- rewritten to point to the temporary schema.
CREATE OR REPLACE FUNCTION auth_lookup_account_by_email(input_email text)
RETURNS text
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
SELECT
pa.id::text AS person_account_id
FROM person_accounts pa
WHERE lower(pa.email_address) = lower(trim(input_email))
LIMIT 1;
$$;

REVOKE ALL ON FUNCTION auth_lookup_account_by_email(text) FROM PUBLIC;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-- Empty schema (no functions or tables)
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"version": "1.0.0",
"pgschema_version": "1.7.3",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085"
},
"groups": [
{
"steps": [
{
"sql": "CREATE TABLE IF NOT EXISTS person_accounts (\n id uuid DEFAULT gen_random_uuid(),\n first_name text,\n last_name text,\n email_address text NOT NULL,\n created_at timestamptz DEFAULT now() NOT NULL,\n modified_at timestamptz DEFAULT now() NOT NULL,\n CONSTRAINT person_accounts_pkey PRIMARY KEY (id),\n CONSTRAINT person_accounts_email_address_key UNIQUE (email_address)\n);",
"type": "table",
"operation": "create",
"path": "public.person_accounts"
},
{
"sql": "CREATE OR REPLACE FUNCTION auth_lookup_account_by_email(\n input_email text\n)\nRETURNS text\nLANGUAGE sql\nSTABLE\nSECURITY DEFINER\nSET search_path = public, pg_temp\nAS $$\n SELECT\n pa.id::text AS person_account_id\n FROM person_accounts pa\n WHERE lower(pa.email_address) = lower(trim(input_email))\n LIMIT 1;\n$$;",
"type": "function",
"operation": "create",
"path": "public.auth_lookup_account_by_email"
},
{
"sql": "REVOKE EXECUTE ON FUNCTION auth_lookup_account_by_email(input_email text) FROM PUBLIC;",
"type": "revoked_default_privilege",
"operation": "create",
"path": "revoked_default.FUNCTION.auth_lookup_account_by_email(input_email text)"
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
CREATE TABLE IF NOT EXISTS person_accounts (
id uuid DEFAULT gen_random_uuid(),
first_name text,
last_name text,
email_address text NOT NULL,
created_at timestamptz DEFAULT now() NOT NULL,
modified_at timestamptz DEFAULT now() NOT NULL,
CONSTRAINT person_accounts_pkey PRIMARY KEY (id),
CONSTRAINT person_accounts_email_address_key UNIQUE (email_address)
);

CREATE OR REPLACE FUNCTION auth_lookup_account_by_email(
input_email text
)
RETURNS text
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
SELECT
pa.id::text AS person_account_id
FROM person_accounts pa
WHERE lower(pa.email_address) = lower(trim(input_email))
LIMIT 1;
$$;

REVOKE EXECUTE ON FUNCTION auth_lookup_account_by_email(input_email text) FROM PUBLIC;
Loading