• 感觉很麻烦
  • 迁移还得自己写过程
  • 建表字段用数组配置表达,还不如原生sql直观呢
  • 太重了,简单封装自建数据表,一两个文件就够了,还更好控制
  • 还有权限控制,学习起来太麻烦

备份一下,使用 codex-5.6-sol,极高推理,1.5x速度 完成的,几乎用了一下午时间,才最终确定自己想要的结构和注释说明

<?php

use BerlinDB\Database\Kern\Query;
use BerlinDB\Database\Kern\Row;
use BerlinDB\Database\Kern\Schema;
use BerlinDB\Database\Kern\Table;

defined('ABSPATH') || exit;

if (!class_exists(Query::class)) return;

/**
 * 2026.08.10
 * 手写继承Row类,显式传入字段时:
 * 1.防止 PHP 8.2 的“弃用警告”( BerlinDB 查出数据后 new Row($数据库记录),把字段写进对象$row->id = 1.... 没有提前声明 $id、$code 等字段,动态写入时会触发弃用警告)。
 * 2.IDE 能自动提示 $row->code
 * 3.即使查询结果缺少某个字段,该属性也依然存在。
 * 4.可以明确字段类型和应用层结构。
 * ----------------
 * 现在封装成通用方法,为了继承者不写继承Row类,这里提供一个默认的通用继承Row类,继承Row类,让 BerlinDB 动态创建属性吗,缺点就是上面显式传入字段的功能就没有了,IDE也不会提示$row->code了。
 * 当然,你可以在继承 WU_DB 时,传入 自己手写的 row_class (必须继承自WU_DB_Row,修复to_array出现内部状态) ,就可以实现上面显式传入字段的优点了。
 * ----------------
 * #[\AllowDynamicProperties]:
 * PHP 8.2 开始,不推荐给对象添加未声明的属性。
 * 因为通用 Row 并没有提前声明 $id、$code 等字段,BerlinDB 动态写入时可能产生弃用警告。这个属性表示:
 * 允许 BerlinDB 根据查询结果动态创建 $row->id、$row->code 等属性。
 */
#[\AllowDynamicProperties]
class WU_DB_Row extends Row {
    /**
     * 只把公开声明或动态生成的业务字段转换为数组。
     * BerlinDB 原始实现会在父类作用域内读取对象变量,导致 args、logs、casts
     * 等受保护的内部状态也出现在结果中。数组强制转换会标记非公开属性,借此
     * 可以稳定排除这些框架内部字段。
     *
     * @return array<string, mixed> 当前 Row 的业务字段。
     */
    public function to_array() {
        $data = [];
        foreach ((array)$this as $key => $value) {
            /**
             * PHP 把对象强制转换为数组时 (array)$this :
             * 公开属性的键保持正常:'id'  'code'
             * 受保护或私有属性的键会被 PHP 加上包含空字符 "\0" 的内部标记,例如概念上类似:
             * "\0*\0args"
             * "\0*\0logs"
             * 因此:if (strpos($key, "\0") !== false) continue;
             * 意思是:键名包含 PHP 内部标记,就跳过,不放进返回数组。
             * 最终只保留公开声明和动态生成的业务字段。
             */
            if (strpos($key, "\0") !== false) continue;
            $data[$key] = $value;
        }
        return $data;
    }
}

/**
 * WPunion BerlinDB 数据表基础类。
 * 子类只需要在 config 中声明数据表、字段、索引和权限配置。基础类负责:
 * 1. 动态创建并缓存 Schema;
 * 2. 注册 Table 以及结构升级钩子;
 * 3. 配置 Query 的表名、缓存组和 Row 类型;
 * 4. 根据 Schema 默认值创建未持久化 Row。
 * -------------------------
 * 建议优先使用 WU_DB 提供的包裹方法
 * @see WU_DB::select
 * @see WU_DB::insert
 * @see WU_DB::delete
 * @see WU_DB::update
 * @see WU_DB::get
 *      -------------------------
 *      abstract 抽象类,只能被继承,不能直接被实例化
 * @see _SAMPLE_WU_DB
 */
abstract class WU_DB extends Query {
    # 当前数据表及查询对象的配置。
    protected $config = [];
    
    # 当前数据表及查询对象的默认配置;子类仅必须配置 name、version、columns。
    private $default_config = [
        'name' => '',
        'version' => '',
        'indexes' => [],
        'columns' => [],
        'column_caps' => [
            'select' => 'manage_options',
            'insert' => 'manage_options',
            'update' => 'manage_options',
            'delete' => 'manage_options',
        ],
        'prefix' => '',
        'description' => '',
        'global' => false,
        'db_version_key' => '',
        'comment' => '',
        'upgrades' => [],
        'table_alias' => '',
        'item_name' => '',
        'item_name_plural' => '',
        'row_class' => WU_DB_Row::class,
        'cache_group' => '',
    ];
    
    
    /** ------------------------------------------------------------- */
    
    /**
     * 查询数据库并获取数据项或数量。
     * @param array<string, mixed>|string $query  查询参数数组或 URL 查询字符串。
     * @return array<int|string, mixed>|int 数据项数组,count=true 时返回数量。
     * @since 1.0.0
     * @since 3.0.0 使用生命周期管理,并由查询解析和数据获取流程完成查询。
     */
    public function select($query = []) {
        return parent::query($query);
    }
    
    /**
     * 按任意 Schema 列和值获取单条数据库记录,结果可能来自缓存。
     * 注意事项:
     * - 只应查询值唯一的列;父类 SQL 使用 LIMIT 1,非唯一列会得到不确定的一条;
     * - 列名必须存在于 Schema,值必须是标量;
     * - 空字符串、0、'0'、null 都会被 empty() 判定为空并直接返回 false;如果确实需要查询这些值,应使用 query();
     * - 成功返回 Row,查不到、列无效或参数无效都返回 false;
     * - 对外接口仍要自行完成业务权限判断。
     */
    public function get($column_value = '', $column_name = 'id') {
        return parent::get_item_by($column_name, $column_value);
    }
    
    /**
     * 向数据库新增一条记录。
     * @param array<string, mixed> $data  要新增的数据。
     * @return int|false 成功返回新数据项 ID,失败返回 false。
     * @since 1.0.0
     */
    public function insert($data = []) {
        return parent::add_item($data);
    }
    
    /**
     * 按主键 ID 更新一条数据库记录。
     * @param int|string           $item_id  要更新的数据项 ID。
     * @param array<string, mixed> $data     需要更新的数据。
     * @return bool 更新成功返回 true,否则返回 false。
     * @since 1.0.0
     */
    public function update($item_id = 0, $data = []) {
        return parent::update_item($item_id, $data);
    }
    
    /**
     * 按主键 ID 从数据库删除一条记录。
     * @param int|string $item_id  要删除的数据项 ID。
     * @return bool 删除成功返回 true,否则返回 false。
     * @since 1.0.0
     */
    public function delete($item_id = 0) {
        return parent::delete_item($item_id);
    }

    /** ------------------------------------------------------------- */

    /**
     * 继承:返回包含 WordPress 站点前缀和插件前缀的完整物理表名。
     * @return string 完整物理表名。
     */
    public function get_table_name() {
        return parent::get_table_name();
    }

    /**
     * 继承:返回最近一次 select() 查询最终执行的 SQL 字符串。
     * @return string 最近一次查询 SQL;尚未查询时返回空字符串。
     */
    public function get_request() {
        return parent::get_request();
    }

    /**
     * 继承:返回当前查询对象收集的诊断日志,并可按日志字段过滤。
     * @param array<string, mixed> $args      可选的日志字段和值。
     * @param string               $operator  字段匹配方式,可选 and、or,默认 and。
     * @return array<int, array<string, mixed>> 符合条件的诊断日志。
     */
    public function get_logs(array $args = [], string $operator = 'and') {
        return parent::get_logs($args, $operator);
    }

    /** ------------------------------------------------------------- */
    
    /**
     * 新增:在文件加载阶段注册数据表,确保 admin_init 能执行安装和升级检查。
     * @return Table 当前数据表的 BerlinDB 管理对象。
     */
    final public static function register() {
        $database = new static();
        return $database->get_table_manager();
    }
    
    /**
     * WU_DB 自有:返回当前实例使用的 Schema 对象。
     * @return Schema 当前数据表结构。
     */
    final public function get_schema() {
        return $this->table_schema;
    }
    
    /**
     * WU_DB 自有:返回当前实例使用的 Table 管理对象。
     * @return Table 当前数据表管理对象。
     */
    final public function get_table_manager() {
        return $this->table_manager;
    }
    
    /**
     * WU_DB 自有:根据 Schema 的 columns.default 动态生成 Row 默认值。
     * BerlinDB 使用 false 表示数据库列没有默认值。Row 仍然保留这个占位值,
     * 因而未持久化的数据行也始终具有 Schema 声明的全部字段。
     *
     * @return array<string, mixed> 以列名为键的 Row 默认值。
     */
    final public function get_row_defaults() {
        $defaults = [];
        
        foreach ($this->get_columns() as $column) {
            if (empty($column->name)) continue;
            $defaults[$column->name] = $column->default;
        }
        
        return $defaults;
    }
    
    /**
     * 新增:根据默认值创建一个未写入数据库的 Row
     * @param array|object $data  要覆盖到默认值上的字段数据。
     * @return Row 当前数据表配置的 Row 对象。
     */
    final public function new_row($data = []) {
        if (is_object($data)) $data = (array)$data;
        if (!is_array($data)) throw new InvalidArgumentException('WU_DB Row 数据必须是数组或对象。');
        
        $row_class = $this->item_shape;
        return new $row_class(array_replace($this->get_row_defaults(), $data));
    }
    
    /** ------------------------------------------------------------- */
    
    # 当前实例使用的 Table 管理对象。
    private $table_manager;
    
    # 按具体 WU_DB 子类缓存的 Schema 对象。
    private static $schema_cache = [];
    
    # 按物理数据表缓存的 Table 管理对象,避免重复注册钩子。
    private static $table_instances = [];
    
    # 已注册物理数据表的结构指纹,用于发现同名表配置冲突。
    private static $table_fingerprints = [];
    
    /**
     * 配置数据表并初始化 BerlinDB Query。
     * @param array|string $query  可选查询参数;传入时沿用 BerlinDB 的构造即查询行为。
     * @return void
     */
    final public function __construct($query = []) {
        if (!is_array($query) && !is_string($query)) throw new InvalidArgumentException('WU_DB 查询参数必须是数组或查询字符串。');
        if (!is_array($this->config)) throw new UnexpectedValueException('WU_DB config 必须是数组。');
        
        // 合并默认配置并统一清理标识符,让错误配置在进入 BerlinDB 前尽早暴露。
        $config = wp_parse_args($this->config, $this->default_config);
        $config['prefix'] = sanitize_key((string)$config['prefix']);
        $config['name'] = sanitize_key((string)$config['name']);
        $config['version'] = is_scalar($config['version']) ? trim((string)$config['version']) : '';
        $config['description'] = is_scalar($config['description']) ? wp_kses_data((string)$config['description']) : '';
        $config['global'] = wp_validate_boolean($config['global']);
        $config['db_version_key'] = sanitize_key((string)$config['db_version_key']);
        $config['comment'] = is_scalar($config['comment']) ? (string)$config['comment'] : '';
        $config['upgrades'] = is_array($config['upgrades']) ? $config['upgrades'] : [];
        $config['table_alias'] = sanitize_key((string)$config['table_alias']);
        $config['item_name'] = sanitize_key((string)$config['item_name']);
        $config['item_name_plural'] = sanitize_key((string)$config['item_name_plural']);
        $config['cache_group'] = sanitize_key((string)$config['cache_group']);
        
        if (!$config['name']) throw new UnexpectedValueException('WU_DB config.name 不能为空。');
        if (!$config['version']) throw new UnexpectedValueException('WU_DB config.version 不能为空。');
        if (!is_array($config['columns']) || !$config['columns']) throw new UnexpectedValueException('WU_DB config.columns 必须声明至少一个字段。');
        if (!is_array($config['indexes'])) throw new UnexpectedValueException('WU_DB config.indexes 必须是数组。');
        if (!is_array($config['column_caps'])) throw new UnexpectedValueException('WU_DB config.column_caps 必须是数组。');
        
        if (!$config['item_name']) $config['item_name'] = $config['name'];
        if (!$config['item_name_plural']) $config['item_name_plural'] = $config['name'];
        if (!$config['cache_group']) $config['cache_group'] = $config['name'];
        
        /*
         * 通用 Table 不认识 WU_DB 子类方法。把方法名包装为绑定当前实例的闭包后,
         * public/protected 升级方法都能由 BerlinDB Table 正常执行。
         */
        $upgrades = [];
        foreach ($config['upgrades'] as $version => $callback) {
            $version = is_scalar($version) ? trim((string)$version) : '';
            if (!$version) throw new UnexpectedValueException('WU_DB config.upgrades 的版本号不能为空。');
            
            if (is_string($callback) && method_exists($this, $callback)) {
                $method = $callback;
                $callback = function () use ($method) {
                    return $this->{$method}();
                };
            }
            
            if (!is_callable($callback)) throw new UnexpectedValueException("WU_DB config.upgrades.{$version} 不是有效回调。");
            $upgrades[$version] = $callback;
        }
        $config['upgrades'] = $upgrades;
        
        $row_class = $config['row_class'];
        if (!is_string($row_class) || !class_exists($row_class) || !is_a($row_class, WU_DB_Row::class, true)) throw new UnexpectedValueException('WU_DB config.row_class 必须是有效的 WU_DB_Row 子类。');
        
        // 每个 WU_DB 子类只解析一次字段和索引配置,后续实例直接复用 Schema。
        $database_class = static::class;
        if (isset(self::$schema_cache[$database_class])) {
            $schema = self::$schema_cache[$database_class];
        } else {
            $columns = [];
            foreach ($config['columns'] as $column) {
                if (is_array($column) && $config['column_caps']) {
                    $column['caps'] = array_merge(
                        $config['column_caps'],
                        isset($column['caps']) && is_array($column['caps']) ? $column['caps'] : []
                    );
                }
                $columns[] = $column;
            }
            
            $schema = new Schema([
                // 数据表的字段定义。
                'columns' => $columns,
                // 数据表的索引定义。
                'indexes' => $config['indexes'],
            ]);
            
            $errors = $schema->get_validation_errors();
            if ($errors) throw new UnexpectedValueException("{$database_class} Schema 无效:" . implode(';', $errors));
            self::$schema_cache[$database_class] = $schema;
        }
        
        // Query 和 Table 共用同一个 Schema,避免同一份字段配置被重复解析。
        $this->prefix = $config['prefix'];
        $this->table_name = $config['name'];
        $this->table_alias = $config['table_alias'];
        $this->table_schema = $schema;
        $this->item_name = $config['item_name'];
        $this->item_name_plural = $config['item_name_plural'];
        $this->item_shape = $config['row_class'];
        $this->cache_group = $config['cache_group'];
        
        // 同一物理表只创建一个 Table 管理对象,避免重复注册升级钩子。
        $scope = $config['global'] ? 'network' : 'site';
        $table_name = $config['prefix'] ? "{$config['prefix']}_{$config['name']}" : $config['name'];
        $table_key = "{$scope}:{$table_name}";
        $fingerprint = hash('sha256', $schema->get_create_table_string() . '|' . $config['version']);
        
        $new_table_manager = false;
        if (isset(self::$table_instances[$table_key])) {
            if (self::$table_fingerprints[$table_key] !== $fingerprint) throw new LogicException("物理数据表 {$table_key} 被配置了不同的 Schema 或版本。");
            $table = self::$table_instances[$table_key];
        } else {
            $table = new Table([
                // 不含任何前缀的数据表名。
                'name' => $config['name'],
                // 插件或模块的数据表前缀。
                'prefix' => $config['prefix'],
                // 当前目标结构版本。
                'version' => $config['version'],
                // 数据表用途说明。
                'description' => $config['description'],
                // 是否注册为多站点全局表。
                'global' => $config['global'],
                // 保存当前结构版本的 option 键。
                'db_version_key' => $config['db_version_key'],
                // 数据表注释。
                'comment' => $config['comment'],
                // 后续结构升级回调。
                'upgrades' => $config['upgrades'],
                // Query 与 Table 共用的 Schema 对象。
                'schema' => $schema,
            ]);
            
            self::$table_instances[$table_key] = $table;
            self::$table_fingerprints[$table_key] = $fingerprint;
            $new_table_manager = true;
        }
        $this->table_manager = $table;
        
        /*
         * 先完成 Query 初始化,让升级回调能取得带 WordPress 前缀的物理表名;
         * 如果调用者在构造时传入查询,则在安装或升级检查完成后再执行该查询。
         */
        parent::__construct();
        if ($new_table_manager && did_action('admin_init')) $table->maybe_upgrade();
        if (!empty($query)) $this->query($query);
    }
    
}

/**
 * WU_DB 完整配置示例。
 * config 顶层只有 name、version、columns 必填,其余键都有默认值并可省略。
 * columns 中每个字段至少需要 name、type;indexes 可省略,但声明后每个索引
 * 必须有 columns,非 primary 索引还必须有 name。
 * -------------------
 * 在插件主文件加载基础类和具体表类后立即注册。Table 会在 admin_init 自动
 * 安装或升级;新站点直接按最新 Schema 建表,不执行历史 upgrades。
 * _SAMPLE_WU_DB::register();
 */
class _SAMPLE_WU_DB extends WU_DB {
    # 示例表配置;实际项目只保留业务需要的字段和索引。
    protected $config = [
        // 必填。数据表基础名;默认 '';只能使用可被 sanitize_key() 保留的字符,不含 $wpdb->prefix 和插件前缀。
        'name' => 'sample_items',
        
        // 必填。目标结构版本;默认 '';可使用 version_compare() 能比较的版本号,Schema 变化时必须递增。
        'version' => '2026081002',
        
        // 必填。字段定义;默认 [];至少一个字段,每项至少声明 name、type。
        'columns' => [
            [
                # 必填。字段名;默认 '';应使用小写字母、数字和下划线,且在当前 Schema 内唯一。
                'name' => 'id',
                # 必填。MySQL 类型;默认 '';
                # 可选整数、浮点数、char/varchar、各级 text/blob、日期时间、json、bool、binary/varbinary。
                'type' => 'bigint',
                # 可选。字段长度;默认 false;ID 常用 20,varchar 索引字段常控制在 191 以内。
                'length' => 20,
                # 可选。整数是否无符号;整数默认 true;false 允许负数。
                'unsigned' => true,
                # 可选。数值是否以零填充;默认 false;通常不建议开启。
                'zerofill' => false,
                # 可选。是否允许 NULL;默认 false;true 时 default 可以为 null。
                'allow_null' => false,
                # 可选。数据库默认值;默认 '';false 表示不生成 DEFAULT 子句。
                'default' => false,
                # 可选。字段附加属性;默认 '';
                # 可选 AUTO_INCREMENT、ON UPDATE CURRENT_TIMESTAMP、SERIAL、SERIAL DEFAULT VALUE。
                'extra' => 'AUTO_INCREMENT',
                # 可选。是否标记为 Query 主字段;默认 false;它不会生成主键 SQL,且与 primary 索引并用会被判为双主键,本例依靠约定的 id 字段回退。
                'primary' => false,
                # 可选。是否建立独立对象缓存键;默认 false;主字段或唯一字段适合开启。
                'cache_key' => true,
                # 可选。是否允许按当前字段排序;默认 false;有索引的数字、短文本和日期字段适合开启。
                'sortable' => true,
                # 可选。wpdb 写入占位符;默认 %s;可选 %s、%d、%f,整数应显式使用 %d。
                'pattern' => '%d',
                # 可选。字段注释;默认 '';只写入数据库结构,不参与业务逻辑。
                'comment' => '自增主键',
            ],
            [
                'name' => 'uuid',
                'type' => 'varchar',
                # 可选。是否启用 UUID 特殊字段;默认 false;true 时新增记录会自动生成 UUID。
                'uuid' => true,
                'default' => '',
                'cache_key' => true,
            ],
            [
                'name' => 'code',
                'type' => 'varchar',
                'length' => 191,
                # 可选。字符串是否使用二进制排序规则;默认 false;true 可让编码比较区分大小写。
                'binary' => true,
                # 可选。字段字符集;默认 '',继承数据库字符集;高级场景可填 ascii、utf8mb4 等。
                'encoding' => '',
                # 可选。字段排序规则;默认 '',继承数据库排序规则;高级场景可填 ascii_bin、utf8mb4_bin 等。
                'collation' => '',
                'default' => '',
                # 可选。是否加入 Query 的 search 模糊搜索字段;默认 false。
                'searchable' => true,
                'sortable' => true,
                # 可选。是否允许 code__in;默认 true;长文本字段建议关闭。
                'in' => true,
                # 可选。是否允许 code__not_in;默认 true;长文本字段建议关闭。
                'not_in' => true,
                'cache_key' => true,
                # 可选。字段值改变时是否触发 BerlinDB transition 动作;默认 false。
                'transition' => true,
                # 可选。写入前验证回调;默认按类型推断;可传任意 callable,这里把编码清理为键名格式。
                'validate' => 'sanitize_key',
                # 可选。旧字段别名;默认 [];可在重命名查询字段时保留旧名称入口,但不会自动改数据库字段。
                'aliases' => [
                    'invite_code',
                    'item_code',
                ],
                # 可选。字段级 CRUD 权限;默认 [] 并继承 column_caps;这里仅覆盖 update。
                'caps' => [
                    # 更新 code 需要的 WordPress capability;覆盖 column_caps.update。
                    'update' => 'manage_options',
                ],
            ],
            [
                'name' => 'valid_days',
                'type' => 'int',
                'length' => 10,
                'unsigned' => true,
                'default' => 365,
                # 可选。读取数据库结果后的类型转换;默认按类型推断;可传任意 callable。
                'cast' => 'absint',
                'validate' => 'absint',
                'pattern' => '%d',
                'sortable' => true,
            ],
            [
                'name' => 'used_by',
                'type' => 'bigint',
                'length' => 20,
                'unsigned' => true,
                'allow_null' => true,
                'default' => null,
                'pattern' => '%d',
                # relationships 默认 [],用于描述跨表关系,但当前 BerlinDB 源码仍标记为待实现,不建议依赖。
                'relationships' => [],
            ],
            [
                'name' => 'used_at',
                'type' => 'datetime',
                'allow_null' => true,
                'default' => null,
                # 可选。是否允许在 date_query 中使用该字段;默认 false。
                'date_query' => true,
                'sortable' => true,
            ],
            [
                'name' => 'status',
                'type' => 'varchar',
                'length' => 20,
                'default' => 'normal',
                'in' => true,
                'not_in' => true,
                'sortable' => true,
                'transition' => true,
                'validate' => 'sanitize_key',
            ],
            [
                'name' => 'note',
                'type' => 'text',
                'allow_null' => true,
                'default' => null,
                'searchable' => true,
                'in' => false,
                'not_in' => false,
                'validate' => 'wp_kses_post',
            ],
            [
                'name' => 'meta',
                'type' => 'json',
                'default' => '{}',
            ],
            [
                'name' => 'created_at',
                'type' => 'datetime',
                'allow_null' => true,
                'default' => null,
                # 可选。是否在新增记录时自动写入当前时间;默认 false;一个 Schema 通常只设一个 created 字段。
                'created' => true,
                'date_query' => true,
                'sortable' => true,
            ],
            [
                'name' => 'updated_at',
                'type' => 'datetime',
                'allow_null' => true,
                'default' => null,
                # 可选。是否在新增和更新记录时自动写入当前时间;默认 false;一个 Schema 通常只设一个 modified 字段。
                'modified' => true,
                'date_query' => true,
                'sortable' => true,
            ],
        ],
        
        // 可选。索引定义;默认 [];支持 primary、unique、key、fulltext,索引字段必须存在于 columns。
        'indexes' => [
            [
                # 可选。索引类型;默认 key;可选 primary、unique、key、fulltext,primary 不需要 name 且整个 Schema 只能有一个主键。
                'type' => 'primary',
                # 必填。索引字段列表;联合索引按数组顺序生成。
                'columns' => ['id'],
            ],
            [
                # 非主键索引必填。索引名称;默认 '';在当前 Schema 内必须唯一。
                'name' => 'code',
                'type' => 'unique',
                'columns' => ['code'],
                # 可选。索引算法;默认 BTREE;常见可选 BTREE、HASH,实际支持取决于数据库引擎。
                'method' => 'BTREE',
                # 可选。索引注释;默认 ''。
                'comment' => '业务编码唯一索引',
            ],
            [
                'name' => 'uuid',
                'type' => 'key',
                'columns' => ['uuid'],
                # 可选。是否强制唯一;默认 false;true 与 type=unique 效果相同。
                'unique' => true,
                # 可选。显式 USING 算法;默认 '';非空时优先于 method,primary 和 fulltext 会忽略它。
                'using' => 'BTREE',
            ],
            [
                'name' => 'status_created_at',
                'type' => 'key',
                'columns' => [
                    'status',
                    'created_at',
                ],
                'method' => 'BTREE',
            ],
            [
                'name' => 'used_by',
                'columns' => ['used_by'],
            ],
            [
                'name' => 'note_search',
                'type' => 'fulltext',
                'columns' => ['note'],
                'comment' => '备注全文索引,仅供显式 MATCH AGAINST 查询使用',
            ],
        ],
        
        // 可选。历史升级回调;默认 [];键为目标版本,值可为 callable 或当前子类 public/protected 方法名。
        'upgrades' => [
            // 从 2026081001 升到 2026081002 时同步完整 Schema,并迁移历史状态值。
            '2026081002' => 'upgrade_2026081002',
        ],
        
        // 可选。插件或模块前缀;默认 '';例如 wu,最终表名为 {$wpdb->prefix}wu_sample_items。
        'prefix' => 'wu',
        
        // 可选。数据表用途说明;默认 '';会传给 BerlinDB Table。
        'description' => 'WU_DB 字段、索引、升级与查询示例表',
        
        // 可选。是否为多站点全局表;默认 false;false 使用 $wpdb->prefix,true 使用网络全局前缀和网络 option。
        'global' => false,
        
        // 可选。保存数据库结构版本的 option 键;默认 '';留空由 BerlinDB 根据表名自动生成。
        'db_version_key' => '',
        
        // 可选。CREATE TABLE 的表注释;默认 '';字符串最长由 BerlinDB 限制为 2048 字符。
        'comment' => 'WU_DB sample table',
        
        // 可选。SQL 表别名;默认 '';留空由 Query 根据前缀和表名自动生成。
        'table_alias' => 'sample',
        
        // 可选。单条数据名称;默认 '',WU_DB 会回退为 name;用于生成 BerlinDB 动作钩子名称。
        'item_name' => 'sample_item',
        
        // 可选。多条数据名称;默认 '',WU_DB 会回退为 name;用于生成 BerlinDB 动作钩子名称。
        'item_name_plural' => 'sample_items',
        
        // 可选。查询结果 Row 类;默认 WU_DB_Row::class;可选任意已加载的 WU_DB_Row 子类。
        'row_class' => WU_DB_Row::class,
        
        // 可选。WordPress 对象缓存组;默认 '',WU_DB 会回退为 name;字段结构不兼容时建议更换缓存组。
        'cache_group' => 'wu_sample_items_v2',
        
        // 可选。所有字段的默认 CRUD 权限;默认四项均为 manage_options;字段自己的 caps 可逐项覆盖。
        'column_caps' => [
            // 读取字段需要的 WordPress capability;默认 manage_options。
            'select' => 'read',
            // 新增字段需要的 WordPress capability;默认 manage_options。
            'insert' => 'edit_posts',
            // 更新字段需要的 WordPress capability;默认 manage_options。
            'update' => 'edit_posts',
            // 删除记录时该字段需要的 WordPress capability;默认 manage_options。
            'delete' => 'delete_posts',
        ],
    ];
    
    /**
     * 把 2026081001 的旧表升级到 2026081002。
     * dbDelta() 幂等地增加或修改字段和索引;数据回填也必须可重复执行,
     * 因为回调失败时 BerlinDB 不会推进版本,下次 admin_init 会再次执行。
     *
     * @return bool 结构同步和历史数据迁移是否全部成功。
     */
    protected function upgrade_2026081002() {
        global $wpdb;
        
        $schema_sql = $this->get_schema()->get_create_table_string();
        if ($schema_sql === '') return false;
        
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        
        // 使用当前完整 Schema,兼容升级中断后的重复执行。
        $table_name = $this->get_table_name();
        $wpdb->last_error = '';
        dbDelta("CREATE TABLE {$table_name} (\n{$schema_sql}\n) {$wpdb->get_charset_collate()};");
        if ($wpdb->last_error !== '') return false;
        
        // dbDelta() 返回的是变更描述而不是成功布尔值,所以显式验证本版本的关键结构。
        $table = $this->get_table_manager();
        if (!$table->column_exists('meta') || !$table->index_exists('note_search')) return false;
        
        // 历史版本曾使用 active,新版本统一为 normal;影响 0 行也属于成功。
        $sql = $wpdb->prepare(
            "UPDATE {$table_name} SET status = %s WHERE status = %s",
            'normal',
            'active'
        );
        if (!is_string($sql)) return false;
        
        return $wpdb->query($sql) !== false;
    }
}