database/gdb: Model.Partition() is a no-op, partition names never reach the generated SQL
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().
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:
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:
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):
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_3The 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:
// gdb.go
// FormatPartitionClause returns the table expression restricting a statement to
// the partitions named by Model.Partition().
FormatPartitionClause(table string, partitions []string) stringThe Core default must ignore the names and return table unchanged, rather
than emit the MySQL form:
// 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
// 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
partitionas[]stringinstead of a comma-joinedstring, so each driver decides how to render it. The public signaturePartition(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 whatAs()already does atgdb_model.go:232-243. It cannot simply be appended tom.tables, which may already contain aliases and JOINs. - Apply it only where the
FROMclause is actually assembled (gdb_model_select.go:767and:788). It must not be threaded through theTablefield of theDo*hook inputs: that value also feeds metadata lookups, andTableFieldsrejects anything containing a space whiledoQuoteTableNamesplits on commas, which manglesPARTITION (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:
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
Source: gogf/gf