Skip to content

fix(Migrator): use lowercase current_schema() for PG14 compatibility - #349

Open
feiguoL wants to merge 1 commit into
go-gorm:masterfrom
feiguoL:fix-current-schema-pg14
Open

fix(Migrator): use lowercase current_schema() for PG14 compatibility#349
feiguoL wants to merge 1 commit into
go-gorm:masterfrom
feiguoL:fix-current-schema-pg14

Conversation

@feiguoL

@feiguoL feiguoL commented Aug 10, 2026

Copy link
Copy Markdown

Motivation

PostgreSQL 14 raises syntax error at or near "CURRENT_SCHEMA" (SQLSTATE 42601) when the migrator executes DROP INDEX and other DDL that references CurrentSchema().

The current code returns clause.Expr{SQL: "CURRENT_SCHEMA()"}. CURRENT_SCHEMA (uppercase) is a reserved keyword in PostgreSQL; PG14 parses the keyword first, then sees () as a syntax error. PG15+ tolerates it, but PG14 does not.

This broke the gorm CI matrix (postgres:14, oldstable) for every recent PR — see e.g. go-gorm/gorm#7837, go-gorm/gorm#7839, go-gorm/gorm#7840 — because tests_all.sh runs go get -u -t ./... and picks up v1.6.2.

Fix

Use the lowercase function form current_schema(), which is valid across all supported PostgreSQL versions (13+).

- return clause.Expr{SQL: "CURRENT_SCHEMA()"}, table
+ return clause.Expr{SQL: "current_schema()"}, table

Verification

  • go build ./... — OK
  • go vet ./... — OK
  • go test ./... — passes

The gorm postgres:14, oldstable job is expected to turn green once this lands and a new tag is released.

@keif888

keif888 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

This PR does not work.
It's not the case of the CURRENT_SCHEMA(), but that postgres does not support using CURRENT_SCHEMA() on index drop or alters.

@feiguoL

feiguoL commented Aug 26, 2026

Copy link
Copy Markdown
Author

@keif888 Thank you for the feedback. You are absolutely right — current_schema() (a function call) cannot be used as an identifier argument in DROP INDEX ?.? / ALTER INDEX ?.? DDL statements, regardless of case. PostgreSQL parses these positions as identifiers, not expressions.

I have reworked the fix to address this properly:

  1. Added resolveSchemaName helper that converts the value returned by CurrentSchema into a concrete schema name string:

    • If it's already a string (explicit schema.table), use it directly.
    • If it's a clause.Expr (the current_schema() fallback), the helper queries SELECT current_schema() to resolve it to an actual name before building the DDL, then passes that concrete name as a clause.Column identifier.
    • If it's nil/empty, the schema prefix is omitted entirely and the statement falls back to unqualified DROP INDEX ? / ALTER INDEX ? RENAME TO ?, relying on search_path (same behavior as v1.6.0).
  2. Updated DropIndex and RenameIndex to use resolveSchemaName so the generated DDL is either:

    • DROP INDEX "schema"."idx" (concrete schema name), or
    • DROP INDEX "idx" (no schema prefix — search_path resolves it)

This restores PG14 compatibility while preserving the non-default-schema qualification added in #340. The query-context callers (GetIndexes, HasIndex, ColumnTypes, etc.) are unaffected since they pass current_schema() as a bind value in WHERE clauses, where function calls are valid.

PTAL.

@keif888

keif888 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

I should have said in my comment that I was writing a PR to address this. My bad...
There are now 2 PR's to fix this.
Similar solutions, although reading yours made me do some more testing, and realize that neither handled someone setting the search_path to empty.

I believe you should use the queryRaw function, rather than calling m.DB.Exec directly, as these calls should bypass the DryRun capability, and execute so that a valid result can be returned via DryRun. Also note that the result from SELECT current_schema() is nullable, so your query will throw a scan error when a NULL is returned.

See #351 which is my attempt to fix this.
I don't know which solution is better, but so long as both handle all the same cases, the maintainers can choose which one they prefer.

These are my updated tests, with the SQL generated and errors if expected in comments. It was run via the Gorm Playground, which was pointed at my version of the postgres driver via the go.mod file.

t.Run("TestWhatHappensWithSchemas", func(t *testing.T) {
		assert.NoError(t, DB.Migrator().AutoMigrate(&PublicSchema{}))
		// CREATE TABLE "public"."public_schema" ("id" bigserial,"created_at" timestamptz,"updated_at" timestamptz,"deleted_at" timestamptz,"std_column" text,PRIMARY KEY ("id"))
		assert.NoError(t, DB.Migrator().DropIndex(&PublicSchema{}, "StdColumn"))
		// DROP INDEX "public"."idx_public_public_schema_std_column"
		assert.NoError(t, DB.Migrator().CreateIndex(&PublicSchema{}, "StdColumn"))
		// CREATE INDEX IF NOT EXISTS "idx_public_public_schema_std_column" ON "public"."public_schema" ("std_column")
		assert.NoError(t, DB.Migrator().RenameIndex(&PublicSchema{}, "idx_public_public_schema_std_column", "idx_public_public_schema_std_column2"))
		// ALTER INDEX "public"."idx_public_public_schema_std_column" RENAME TO "idx_public_public_schema_std_column2"
		assert.NoError(t, DB.Migrator().RenameIndex(&PublicSchema{}, "idx_public_public_schema_std_column2", "idx_public_public_schema_std_column"))
		// ALTER INDEX "public"."idx_public_public_schema_std_column2" RENAME TO "idx_public_public_schema_std_column"
		assert.Error(t, DB.Migrator().AutoMigrate(&DotSchema{}))
		// CREATE TABLE "."dot_schema" ("id" bigserial,"created_at" timestamptz,"updated_at" timestamptz,"deleted_at" timestamptz,"std_column" text,PRIMARY KEY ("id")) // ERROR: syntax error at or near "dot_schema" (SQLSTATE 42601)
		require.NoError(t, DB.Exec("set search_path = ''").Error)
		// set search_path = ''
		require.Error(t, DB.Migrator().AutoMigrate(&NoSchema{}))
		// CREATE TABLE "no_schema" ("id" bigserial,"created_at" timestamptz,"updated_at" timestamptz,"deleted_at" timestamptz,"std_column" text,PRIMARY KEY ("id")) // ERROR: no schema has been selected to create in (SQLSTATE 3F000)
		require.NoError(t, DB.Exec("set search_path = DEFAULT").Error)
		// set search_path = DEFAULT
		require.NoError(t, DB.Migrator().AutoMigrate(&NoSchema{}))
		// CREATE TABLE "no_schema" ("id" bigserial,"created_at" timestamptz,"updated_at" timestamptz,"deleted_at" timestamptz,"std_column" text,PRIMARY KEY ("id"))
		assert.NoError(t, DB.Migrator().DropIndex(&NoSchema{}, "StdColumn"))
		// DROP INDEX "public"."idx_no_schema_std_column"
		assert.NoError(t, DB.Migrator().CreateIndex(&NoSchema{}, "StdColumn"))
		// CREATE INDEX IF NOT EXISTS "idx_no_schema_std_column" ON "no_schema" ("std_column")
		assert.NoError(t, DB.Migrator().RenameIndex(&NoSchema{}, "idx_no_schema_std_column", "idx_no_schema_std_column2"))
		// ALTER INDEX "public"."idx_no_schema_std_column" RENAME TO "idx_no_schema_std_column2"
		assert.NoError(t, DB.Migrator().RenameIndex(&NoSchema{}, "idx_no_schema_std_column2", "idx_no_schema_std_column"))
		// ALTER INDEX "public"."idx_no_schema_std_column2" RENAME TO "idx_no_schema_std_column"
		require.NoError(t, DB.Exec("set search_path = ''").Error)
		// set search_path = ''
		assert.Error(t, DB.Migrator().DropIndex(&NoSchema{}, "StdColumn"))
		// DROP INDEX "idx_no_schema_std_column" // ERROR: index "idx_no_schema_std_column" does not exist (SQLSTATE 42704)
		require.NoError(t, DB.Exec("set search_path = DEFAULT").Error)
		// set search_path = DEFAULT
		assert.NoError(t, DB.Migrator().DropIndex(&NoSchema{}, "StdColumn"))
		// DROP INDEX "public"."idx_no_schema_std_column"
		t.Cleanup(func() {
			if DB.Migrator().HasTable(&PublicSchema{}) { // SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'public_schema' AND table_type = 'BASE TABLE'
				require.NoError(t, DB.Migrator().DropTable(&PublicSchema{}))
				// DROP TABLE IF EXISTS "public"."public_schema" CASCADE
			}
			if DB.Migrator().HasTable(&NoSchema{}) { // SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'no_schema' AND table_type = 'BASE TABLE'
				require.NoError(t, DB.Migrator().DropTable(&NoSchema{}))
				// DROP TABLE IF EXISTS "no_schema" CASCADE
			}
			if DB.Migrator().HasTable(&DotSchema{}) { // SELECT count(*) FROM information_schema.tables WHERE table_schema = '' AND table_name = 'dot_schema' AND table_type = 'BASE TABLE'
				require.NoError(t, DB.Migrator().DropTable(&DotSchema{}))
			}
		})
	})
type NoSchema struct {
	gorm.Model
	StdColumn string `gorm:"index"`
}

func (t *NoSchema) TableName() string {
	return "no_schema"
}

type DotSchema struct {
	gorm.Model
	StdColumn string `gorm:"index"`
}

func (t *DotSchema) TableName() string {
	return ".dot_schema"
}

type PublicSchema struct {
	gorm.Model
	StdColumn string `gorm:"index"`
}

func (t *PublicSchema) TableName() string {
	return "public.public_schema"
}

CURRENT_SCHEMA (uppercase) is a reserved keyword in PostgreSQL. When used
as CURRENT_SCHEMA() in DDL contexts like DROP INDEX, PostgreSQL 14 raises
'syntax error at or near CURRENT_SCHEMA' (SQLSTATE 42601) because it
parses the keyword first, not the function call.

Using lowercase current_schema() is the function form that works across all
supported PostgreSQL versions (13+).
@feiguoL
feiguoL force-pushed the fix-current-schema-pg14 branch from 489079c to 29f5a4f Compare August 28, 2026 03:09
@feiguoL

feiguoL commented Aug 28, 2026

Copy link
Copy Markdown
Author

@keif888
Thank you for the detailed feedback and test cases. I have reworked the fix to address all your points:

  1. Use queryRaw instead of m.DB.Raw — Now CurrentSchema uses m.queryRaw("SELECT current_schema()"), which bypasses DryRun mode so a valid result is returned (same pattern as CurrentDatabase()).

  2. Handle NULL from current_schema() (empty search_path) — The query result is scanned into *string; when nil (empty search_path), CurrentSchema returns "", which causes DropIndex/RenameIndex to omit the schema prefix and use unqualified DROP INDEX ? / ALTER INDEX ? RENAME TO ?.

  3. Fix at CurrentSchema level — Instead of resolving in each DDL method, the fix is in CurrentSchema itself so all callers benefit. The returned schema is now always a concrete string (or ""), never a clause.Expr function call. This means:

    • DDL callers (DropIndex, RenameIndex) get a proper identifier string for clause.Column{Name: schema}.
    • Query callers (GetTables, HasIndex, ColumnTypes, GetIndexes) get a string bind value for WHERE schemaname = ?, which works identically.
  4. Removed the resolveSchemaName helper — No longer needed since CurrentSchema returns a string directly.

The generated SQL now matches your test expectations:

  • DROP INDEX "public"."idx_name" (normal case)
  • DROP INDEX "idx_name" (empty search_path — falls back to unqualified, PG errors as expected)
  • ALTER INDEX "public"."old" RENAME TO "new" (normal case)
  • ALTER INDEX "old" RENAME TO "new" (empty search_path)

Our two PRs now use essentially the same approach. Happy to defer to #351 if the maintainers prefer it — the important thing is getting the fix merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants