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.

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 accd563 to 489079c Compare August 26, 2026 05:01
@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"
}

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