#4826·gf

database/gdb: Model.Partition() is a no-op, partition names never reach the generated SQL

Author: lingcoderCreated Aug 3, 2026Updated Aug 4, 2026
Labelsbughelp wanteddiscuss

Go version

go version go1.26.5 windows/amd64

GoFrame version

v2.10.2

Can this bug be reproduced with the latest release?

Yes

What did you do?

Created a partitioned table and queried a single partition through Model.Partition().

go
db.Exec(ctx, `CREATE TABLE t (id int, tag varchar(10))
    PARTITION BY RANGE (id) (
        PARTITION p0 VALUES LESS THAN (100),
        PARTITION p1 VALUES LESS THAN (200),
        PARTITION p2 VALUES LESS THAN MAXVALUE)`)

db.Model("t").Data(g.List{
    g.Map{"id": 1,   "tag": "in_p0"},
    g.Map{"id": 150, "tag": "in_p1"},
    g.Map{"id": 500, "tag": "in_p2"},
}).Insert()

db.Model("t").Partition("p0").All()

What did you see happen?

Every call returns the whole table, and an unknown partition name is accepted without error:

call rows returned
All() (no Partition) 3 in_p0, in_p1, in_p2
Partition("p0").All() 3 in_p0, in_p1, in_p2
Partition("p1").All() 3 in_p0, in_p1, in_p2
Partition("nonexistent").All() 3 in_p0, in_p1, in_p2

CatchSQL shows no PARTITION clause is emitted at all:

SELECT * FROM `t`

The unknown name does not error precisely because nothing is sent — MySQL rejects a bad partition name with Unknown partition 'nonexistent' when the clause is actually present.

This fails silently: no error, no warning. Code that looks correct quietly reads every partition. The same happens on every driver, not just MySQL.

What did you expect to see?

Partition("p0") should return 1 row (in_p0), Partition("p1") should return 1 row (in_p1), and Partition("nonexistent") should return an error.


Root cause

database/gdb/gdb_model.go:

go
func (m *Model) Partition(partitions ...string) *Model {
	model := m.getModel()
	model.partition = gstr.Join(partitions, ",")
	return model
}

The partition field has exactly two references in the whole repository — the declaration at gdb_model.go:40 and the assignment above at gdb_model.go:189. It is never read. SQL generation uses m.tables, which Partition() does not touch.

The API was introduced in #2989 with the setter only; the reader was never added, so it has never worked.

Why CI never caught it

The test added alongside it seeds 5 rows and asserts:

go
data, _ := db3.Model("dbx_order").Partition("p3", "p4").All()
t.Assert(dataLen, 5)
data, _ = db3.Model("dbx_order").Partition("p3").All()
t.Assert(dataLen, 5)

Two different partition sets, the same expected count — and 5 is the entire table. The assertions hold only because the filter is ignored.

The mysql and mariadb suites already carry TODO: Add PARTITION clause support to GoFrame query builder and skip the two cases that depend on the API. The pgsql suite skips all eight.

Dialect behaviour

Measured rather than assumed:

PARTITION (p0) several names unknown name
MySQL 8.0 real pruning PARTITION (p0,p1) accepted errors
GaussDB 7.0 (openGauss) real pruning syntax error errors
PostgreSQL 18 not a partition clause silently accepted

PostgreSQL is the trap. It parses FROM t PARTITION (p0) as a table alias PARTITION plus a column-alias list (p0):

sql
EXPLAIN SELECT * FROM t PARTITION (t_p0);

 Append
   ->  Seq Scan on t_p0 partition_1
   ->  Seq Scan on t_p1 partition_2     -- all partitions scanned
   ->  Seq Scan on t_p2 partition_3

The statement succeeds, returns the whole table, renames the first column, and accepts a nonexistent partition name without complaint. FROM t AS "PARTITION" (renamed_id) produces identical output.

Proposed fix

Three parts. I prototyped this and verified it end to end against MySQL 8.0, PostgreSQL 18 and openGauss 7.0; happy to open a PR if the direction is agreed.

1. New driver-dispatch method on DB

Following the canonical OrderRandomFunction() / GetBoolLiteral() / GetLockSharedClause() pattern:

go
// gdb.go
// FormatPartitionClause returns the table expression restricting a statement to
// the partitions named by Model.Partition().
FormatPartitionClause(table string, partitions []string) string

The Core default must ignore the names and return table unchanged, rather than emit the MySQL form:

go
// gdb_core_underlying.go
func (c *Core) FormatPartitionClause(table string, _ []string) string {
	return table
}

This is the opposite of the usual "Core default preserves MySQL legacy behaviour" choice, and deliberately so: PostgreSQL does not reject the MySQL form, it silently returns the whole table with a renamed column. Defaulting to ignore means a driver that has not opted in stays at today's behaviour instead of producing silently wrong results, and no existing driver needs changing.

2. Driver support

go
// mysql_dialect.go — mariadb inherits via its embedded *mysql.Driver
func (d *Driver) FormatPartitionClause(table string, partitions []string) string {
	if len(partitions) == 0 {
		return table
	}
	return fmt.Sprintf("%s PARTITION (%s)", table, gstr.Join(partitions, ","))
}

// gaussdb_dialect.go — GaussDB accepts exactly one name
func (d *Driver) FormatPartitionClause(table string, partitions []string) string {
	if len(partitions) == 0 {
		return table
	}
	return fmt.Sprintf("%s PARTITION (%s)", table, partitions[0])
}

pgsql, sqlite, mssql, oracle, dm and clickhouse need no changes — they inherit the Core default and keep ignoring the names. Since every driver embeds *gdb.Core, adding the interface method breaks no existing implementation, including third-party ones.

Open question for GaussDB: with more than one name, emit only the first (as above) or return an error?

3. Model implementation

  • Store partition as []string instead of a comma-joined string, so each driver decides how to render it. The public signature Partition(partitions ...string) is unchanged.
  • Add a helper that injects the clause between the table name and its alias — user PARTITION (p0) AS u LEFT JOIN ... — mirroring what As() already does at gdb_model.go:232-243. It cannot simply be appended to m.tables, which may already contain aliases and JOINs.
  • Apply it only where the FROM clause is actually assembled (gdb_model_select.go:767 and :788). It must not be threaded through the Table field of the Do* hook inputs: that value also feeds metadata lookups, and TableFields rejects anything containing a space while doQuoteTableName splits on commas, which mangles PARTITION (p0,p1). 4. Behaviour when the restriction cannot be honoured

Two cases where the partition names cannot be applied, deliberately handled differently:

Query on a driver without partition-selection support — ignore the names and emit a warning through DB.GetLogger(). The query still runs and returns every partition, which is what happens today, so nothing breaks; the warning is what makes the no-op discoverable instead of silent.

The warning belongs in Partition() itself rather than at SQL-assembly time. The driver is already known there, and it fires once per chain construction instead of once per execution, which would flood a query inside a loop.

Write statement — return an error. A log line is not a guardrail here:

go
db.Model("orders").Partition("p_2023").Delete()

If the clause is dropped with only a warning, that deletes the entire table and the warning arrives after the data is gone. A query that over-returns is visible and recoverable; a delete that over-matches is neither. This also matches how the codebase already treats unsupported operations — see errUnsupportedInsertIgnore / errUnsupportedInsertGetId / errUnsupportedReplace in contrib/drivers/clickhouse/clickhouse.go, which error rather than warn.

Existing code calling Partition() on a write path is already broken today — it believes it is restricting the statement while it is not — so failing loudly is an improvement over continuing silently.

case behaviour
supported driver, query clause applied
unsupported driver, query names ignored + warning log
any driver, write statement error

The underlying rule: when a restriction the caller asked for cannot be applied, warn and continue if the consequence is recoverable, and refuse if it is not.

Once wired up, the skipped partition cases in the mysql, mariadb and pgsql suites can be filled in.

Environment

  • GoFrame master (d08a238c2)
  • Verified against MySQL 8.0.46, PostgreSQL 18.4, openGauss 7.0.0-RC1