Skip to content

Enhance currency field type with multi-currency support and configurable column splitting#98

Merged
hotlong merged 4 commits intomainfrom
copilot/automated-column-splitting
Jan 24, 2026
Merged

Enhance currency field type with multi-currency support and configurable column splitting#98
hotlong merged 4 commits intomainfrom
copilot/automated-column-splitting

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jan 24, 2026

Enhances the existing currency field type with multi-currency support using physical column splitting: one logical field maps to 1-2 database columns depending on currency mode.

Schema Additions

CurrencyConfigSchema

  • precision: 0-10 decimal places (default: 2)
  • currencyMode: 'dynamic' (user-selectable) or 'fixed' (single currency)
  • defaultCurrency: 3-character code (default: 'CNY')

CurrencyValueSchema

  • Runtime structure: { value: number, currency: string }

FieldSchema Extension

  • Added optional currencyConfig property

Column Splitting Strategy

Dynamic mode (default): 2 physical columns

  • {fieldName} → DECIMAL(18, precision)
  • {fieldName}_cur → VARCHAR(3)

Fixed mode: 1 physical column

  • {fieldName} → DECIMAL(18, precision)
  • Currency hardcoded in config, no storage overhead

Usage

import { Field } from '@objectstack/spec/data';

// Dynamic currency - generates 2 columns
const price = Field.currency({
  name: 'price',
  label: 'Price',
  currencyConfig: {
    precision: 2,
    currencyMode: 'dynamic',
    defaultCurrency: 'USD'
  }
});

// Fixed currency - generates 1 column, saves space
const salary = Field.currency({
  name: 'salary',
  label: 'Annual Salary',
  currencyConfig: {
    currencyMode: 'fixed',
    defaultCurrency: 'CNY'
  }
});

Design Notes

Currency validation uses length-only (3 chars) to support ISO 4217, cryptocurrencies (BTC, ETH), and custom business codes. Stricter validation deferred to application layer per business requirements.

Enables direct SQL queries on currency: SELECT * FROM projects WHERE price_cur = 'USD' AND price > 10000 with indexing support on both columns.

Original prompt

这是一个基于**“物理列自动分裂(Automated Physical Column Splitting)”**策略的完整技术设计方案。
该方案的核心理念是:UI 层的“单字段”体验,DB 层的“多字段”存储,中间通过引擎自动转换。
系统设计文档:低代码平台多币种字段 (Money Type)

  1. 核心架构图 (Architecture Overview)
    数据流向分为三个阶段:
  • 定义态 (Definition): 用户配置 1 个字段。
  • 存储态 (Storage): 数据库自动生成 2 个物理列。
  • 运行态 (Runtime): 引擎在 Read/Write 时自动进行“装箱”与“拆箱”。
  1. 协议层设计 (Schema Definition)
    我们需要在 grid.zod.ts 中定义货币的元数据。这决定了引擎如何处理这个字段。
    2.1 扩展 Zod Schema
    import { z } from 'zod';

// 货币专用配置
const MoneyConfigSchema = z.object({
// 精度:默认 2 位
precision: z.number().int().default(2),

// 模式:
// 'dynamic': 允许用户选币种 (数据库生成 _cur 列)
// 'fixed': 固定币种 (数据库不生成 _cur 列,节省空间)
currencyMode: z.enum(['dynamic', 'fixed']).default('dynamic'),

// 如果是 fixed 模式,必须指定币种
defaultCurrency: z.string().default('CNY'),
});

export const GridColumnSchema = z.object({
// 字段名 (逻辑名),例如 "contract_amount"
dataIndex: z.string(),
title: z.string(),

// 类型标识
valueType: z.enum(['text', 'number', 'money', 'date']), // ...其他类型

// 挂载配置
moneyConfig: MoneyConfigSchema.optional(),
});

  1. 数据库层设计 (Storage & DDL)
    这是最关键的“脏活”部分。当用户点击“发布表单”时,后端引擎需要解析 Schema 并生成 SQL。
    3.1 命名规范 (Naming Convention)
  • 主列 (Main Column): {fieldName} -> 存储数值 (DECIMAL)
  • 伴生列 (Sidekick Column): {fieldName}_cur -> 存储币种 (VARCHAR)
    3.2 DDL 生成器伪代码 (DDL Generator)
    function generateCreateTableSQL(tableName: string, columns: GridColumnSchema[]) {
    let fieldDefinitions = [];

columns.forEach(col => {
// 处理 Money 类型
if (col.valueType === 'money') {
const { precision = 2, currencyMode = 'dynamic' } = col.moneyConfig || {};

  // 1. 创建主数值列 (存放 1000.00)
  // DECIMAL(18, 2) 是金融标准
  fieldDefinitions.push(
    `\`${col.dataIndex}\` DECIMAL(18, ${precision}) NULL COMMENT '${col.title}'`
  );

  // 2. 动态币种模式下,自动追加伴生列 (存放 'USD')
  if (currencyMode === 'dynamic') {
    fieldDefinitions.push(
      `\`${col.dataIndex}_cur\` VARCHAR(3) DEFAULT 'CNY' COMMENT '${col.title} (币种)'`
    );
  }
} 
// ... 处理其他类型 (text, date 等)
else {
  fieldDefinitions.push(`\`${col.dataIndex}\` VARCHAR(255) NULL`);
}

});

return CREATE TABLE ${tableName} ( id INT PRIMARY KEY, ${fieldDefinitions.join(', ')} );;
}

效果: 用户只定义了 price,但在数据库里你可以直接用 SQL 查 SELECT * FROM table WHERE price_cur = 'USD'。

  1. 引擎层设计 (Runtime Engine)
    引擎充当 ORM (Object-Relational Mapper) 的角色,负责“欺骗”前端。
    4.1 写入转换 (Write / Unboxing)
    前端发来的是一个对象,后端要拆散存入数据库。
    // Input: 前端提交的 JSON
    const payload = {
    title: "服务器采购",
    price: { value: 5000.00, currency: "USD" } // 复合对象
    };

async function insertRecord(tableSchema, payload) {
const dbRow = {};

tableSchema.columns.forEach(col => {
const key = col.dataIndex;
const value = payload[key];

if (col.valueType === 'money') {
  if (value) {
    // 拆箱操作
    dbRow[key] = value.value;           // price = 5000
    dbRow[`${key}_cur`] = value.currency; // price_cur = "USD"
  } else {
    dbRow[key] = null;
    dbRow[`${key}_cur`] = null;
  }
} else {
  // 普通字段
  dbRow[key] = value;
}

});

// 生成 SQL: INSERT INTO ... (title, price, price_cur) VALUES ...
return await db.insert(dbRow);
}

4.2 读取转换 (Read / Boxing)
数据库查出来是散的,后端要组装好发给前端。
// Input: 数据库查出的 Raw Data
const dbRows = [
{ id: 1, title: "服务器采购", price: 5000.00, price_cur: "USD" }
];

function transformRows(tableSchema, dbRows) {
return dbRows.map(row => {
const apiRow = { ...row };

tableSchema.columns.forEach(col => {
  if (col.valueType === 'money') {
    const key = col.dataIndex;
    
    // 装箱操作:合并两个字段为一个对象
    apiRow[key] = {
      value: row[key],            // 5000
      currency: row[`${key}_cur`] // "USD"
    };
    
    // 清理掉伴生列,保证 API 纯净
    delete apiRow[`${key}_cur`];
  }
});

return apiRow;

});
}

  1. 查询构建器 (Query Builder)
    解决“用户如何筛选”的问题。
    用户在前端配置筛选器:{ field: "price", operator: ">", value: 100 }。
    用户的直觉是“金额大于100”,他通常不管币种,或者默认指当前币种。
    function buildWhereClause(filter) {
    const { field, operator, value } = filter;
    // 假设 schema 中 price 是 money 类型

// 场景 1: 用户只查数值 (WHERE price > 100)
// 这是最自然的 SQL 映射,直接查主列
if (typeof value === 'number') {
return \${field}` ${operator} ${value}`;
}

// 场景 2: 用户精确查特定币种的金额 (WHERE price = 100 AND price_cur = 'USD')
// 前端传来的 value 可能是 { value: 100, currency: 'USD' }
if (typeof value === 'object') {
return (\${field}` ${operator} ${value.value} AND `${field}_cur` = '${value.currency}')`;
}
}

  1. 前端组件实现 (React示例)
    前端拿到的数据已经是 { value: 100, currency: 'USD' },组件渲染就非常简单了。
    6.1 MoneyInput 组件
    import { Select, InputNumber } from 'antd';

const MoneyInput = ({ value = {}, onChange }) => {
// value 格式: { value: 100, currency: 'CNY' }

const handleCurrencyChange = (newCur) => {
onChange({ ...value, currency: newCur });
};

const handleValueChange = (newVal) => {
onChange({ ...value, value: newVal });
};

return (
<div style={{ display: 'flex' }}>
<Select
value={value.curr...


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@vercel
Copy link
Copy Markdown

vercel bot commented Jan 24, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
spec Error Error Jan 24, 2026 8:33am

Request Review

- Add 'money' to FieldType enum
- Create MoneyConfigSchema with precision, currencyMode, and defaultCurrency
- Create MoneyValueSchema for runtime value structure
- Extend FieldSchema with optional moneyConfig property
- Add Field.money() helper factory function
- Add comprehensive tests (71 total field tests, all passing)
- Generate JSON schemas and documentation
- All 1638 tests passing

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
- Document flexible currency code validation approach
- Support ISO 4217, cryptocurrency, and custom codes
- Explain validation can be stricter at application layer
- All tests passing, no security vulnerabilities

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement automated physical column splitting strategy Add Money field type with configurable column splitting strategy Jan 24, 2026
Copilot AI requested a review from hotlong January 24, 2026 08:23
@hotlong
Copy link
Copy Markdown
Contributor

hotlong commented Jan 24, 2026

错了,我想改的就是标准的currency类型,不是增加新的类型 @copilot

- Remove 'money' field type from enum (use existing 'currency' instead)
- Rename MoneyConfig to CurrencyConfig
- Rename MoneyValue to CurrencyValue
- Change moneyConfig to currencyConfig in FieldSchema
- Update all tests to use currency type instead of money type
- Remove Field.money() helper (use Field.currency() instead)
- Delete MoneyConfig/MoneyValue schemas and docs
- Generate new CurrencyConfig/CurrencyValue schemas and docs
- All 1638 tests passing

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jan 24, 2026

错了,我想改的就是标准的currency类型,不是增加新的类型 @copilot

已修正。现在多币种支持已应用到标准的 currency 字段类型,而不是创建新的 money 类型。

主要更改:

  • 移除了 'money' 字段类型
  • MoneyConfig 重命名为 CurrencyConfig
  • moneyConfig 改为 currencyConfig
  • 更新所有测试使用 currency 类型

使用示例:

Field.currency({
  name: 'price',
  currencyConfig: {
    precision: 2,
    currencyMode: 'dynamic',
    defaultCurrency: 'USD'
  }
})

提交: 0a470eb

Copilot AI changed the title Add Money field type with configurable column splitting strategy Enhance currency field type with multi-currency support and configurable column splitting Jan 24, 2026
@github-actions github-actions bot added documentation Improvements or additions to documentation protocol:data tests size/l labels Jan 24, 2026
@hotlong hotlong marked this pull request as ready for review January 24, 2026 09:16
Copilot AI review requested due to automatic review settings January 24, 2026 09:16
@hotlong hotlong merged commit bc393be into main Jan 24, 2026
14 of 15 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the existing currency field type with multi-currency support using a physical column splitting strategy. It introduces two new schemas (CurrencyConfigSchema and CurrencyValueSchema) that enable currency fields to support either dynamic (user-selectable) or fixed (single) currency modes, with different storage strategies for each.

Changes:

  • Added CurrencyConfigSchema with configurable precision (0-10), currency mode (dynamic/fixed), and default currency code
  • Added CurrencyValueSchema for runtime representation of currency values with amount and currency code
  • Extended FieldSchema with optional currencyConfig property for currency field configuration
  • Comprehensive test suite covering all new schemas and edge cases including cryptocurrency support

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/spec/src/data/field.zod.ts Added CurrencyConfigSchema and CurrencyValueSchema with proper validation, extended FieldSchema with currencyConfig property, added type exports
packages/spec/src/data/field.test.ts Comprehensive test coverage for CurrencyConfigSchema, CurrencyValueSchema, and currency field integration including edge cases
packages/spec/package.json Version bump from 0.3.1 to 0.3.2
packages/spec/package-lock.json Updated lockfile version references
packages/spec/json-schema/data/CurrencyConfig.json Generated JSON schema for CurrencyConfig matching Zod schema
packages/spec/json-schema/data/CurrencyValue.json Generated JSON schema for CurrencyValue matching Zod schema
packages/spec/json-schema/data/Field.json Updated with currencyConfig property definition
packages/spec/json-schema/data/Object.json Updated with currencyConfig property definition
packages/spec/json-schema/ui/FieldWidgetProps.json Updated with currencyConfig property definition
content/docs/references/data/field/Field.mdx Added currencyConfig property to field reference documentation
content/docs/references/data/field/CurrencyConfig.mdx New reference documentation for CurrencyConfig schema
content/docs/references/data/field/CurrencyValue.mdx New reference documentation for CurrencyValue schema
Files not reviewed (1)
  • packages/spec/package-lock.json: Language not supported

Comment on lines +62 to +73
* Configuration for currency field type supporting multi-currency
*
* Note: Currency codes are validated by length only (3 characters) to support:
* - Standard ISO 4217 codes (USD, EUR, CNY, etc.)
* - Cryptocurrency codes (BTC, ETH, etc.)
* - Custom business-specific codes
* Stricter validation can be implemented at the application layer based on business requirements.
*/
export const CurrencyConfigSchema = z.object({
precision: z.number().int().min(0).max(10).default(2).describe('Decimal precision (default: 2)'),
currencyMode: z.enum(['dynamic', 'fixed']).default('dynamic').describe('Currency mode: dynamic (user selectable) or fixed (single currency)'),
defaultCurrency: z.string().length(3).default('CNY').describe('Default or fixed currency code (ISO 4217, e.g., USD, CNY, EUR)'),
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing documentation in content/docs/guides/field-types.mdx shows currency fields using precision and scale as direct field properties (lines 192-193), but the new implementation introduces currencyConfig.precision. This creates potential confusion about which approach users should follow.

While both approaches are technically valid since FieldSchema supports both general precision/scale properties and the new currencyConfig, the documentation should be updated to clarify:

  1. Whether currencyConfig.precision should be preferred for currency fields
  2. The relationship between field-level precision/scale and currencyConfig.precision
  3. Example usage showing the new multi-currency features (dynamic vs fixed modes, currency selection)

Consider updating the field-types guide to demonstrate the new multi-currency capabilities introduced in this PR.

Suggested change
* Configuration for currency field type supporting multi-currency
*
* Note: Currency codes are validated by length only (3 characters) to support:
* - Standard ISO 4217 codes (USD, EUR, CNY, etc.)
* - Cryptocurrency codes (BTC, ETH, etc.)
* - Custom business-specific codes
* Stricter validation can be implemented at the application layer based on business requirements.
*/
export const CurrencyConfigSchema = z.object({
precision: z.number().int().min(0).max(10).default(2).describe('Decimal precision (default: 2)'),
currencyMode: z.enum(['dynamic', 'fixed']).default('dynamic').describe('Currency mode: dynamic (user selectable) or fixed (single currency)'),
defaultCurrency: z.string().length(3).default('CNY').describe('Default or fixed currency code (ISO 4217, e.g., USD, CNY, EUR)'),
* Configuration for the `currency` field type, including multi-currency behavior.
*
* Precision & scale:
* - For currency fields, `currencyConfig.precision` SHOULD be treated as the
* authoritative decimal precision.
* - Generic field-level `precision` / `scale` (on the parent FieldSchema) remain
* available for backward compatibility and for non-currency numeric fields, but
* tooling and documentation SHOULD prefer `currencyConfig.precision` when the
* field `type === 'currency'`.
*
* Multi-currency behavior:
* - `currencyMode: 'dynamic'` each record/value can specify its own currency
* (see CurrencyValueSchema.currency), with `defaultCurrency` used as the
* initial/default selection.
* - `currencyMode: 'fixed'` all values for the field use a single, fixed
* currency equal to `defaultCurrency`.
*
* Currency codes:
* - Validated only by length (3 characters) to support:
* - Standard ISO 4217 codes (USD, EUR, CNY, etc.)
* - Cryptocurrency codes (BTC, ETH, etc.)
* - Custom business-specific codes
* - Stricter validation can be implemented at the application layer based on
* business requirements.
*/
export const CurrencyConfigSchema = z.object({
precision: z
.number()
.int()
.min(0)
.max(10)
.default(2)
.describe('Currency-specific decimal precision (default: 2)'),
currencyMode: z
.enum(['dynamic', 'fixed'])
.default('dynamic')
.describe('Currency mode: dynamic (per-value currency) or fixed (single currency for the field)'),
defaultCurrency: z
.string()
.length(3)
.default('CNY')
.describe('Default or fixed 3-letter currency code (e.g., USD, CNY, EUR)'),

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/l tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants