feat: 完善类型支持,在pg中提供部分ddl支持

This commit is contained in:
Pan Qiancheng 2026-01-20 18:03:33 +08:00
parent 5a5ac5c194
commit 49dc9141de
15 changed files with 1222 additions and 81 deletions

17
lib/MySQL/store.d.ts vendored
View File

@ -1,4 +1,4 @@
import { EntityDict, OperateOption, OperationResult, TxnOption, StorageSchema, SelectOption, AggregationResult, Attribute, Index } from 'oak-domain/lib/types';
import { EntityDict, OperateOption, OperationResult, TxnOption, StorageSchema, SelectOption, AggregationResult } from 'oak-domain/lib/types';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { CascadeStore } from 'oak-domain/lib/store/CascadeStore';
import { MySQLConfiguration } from './types/Configuration';
@ -7,7 +7,7 @@ import { MySqlTranslator, MySqlSelectOption, MysqlOperateOption } from './transl
import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
import { CreateEntityOption } from '../types/Translator';
import { DbStore } from '../types/dbStore';
import { DbStore, Plan } from '../types/dbStore';
export declare class MysqlStore<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>> extends CascadeStore<ED> implements DbStore<ED, Cxt> {
protected countAbjointRow<T extends keyof ED, OP extends SelectOption, Cxt extends SyncContext<ED>>(entity: T, selection: Pick<ED[T]['Selection'], 'filter' | 'count'>, context: Cxt, option: OP): number;
protected aggregateAbjointRowSync<T extends keyof ED, OP extends SelectOption, Cxt extends SyncContext<ED>>(entity: T, aggregation: ED[T]['Aggregation'], context: Cxt, option: OP): AggregationResult<ED[T]['Schema']>;
@ -48,16 +48,3 @@ export declare class MysqlStore<ED extends EntityDict & BaseEntityDict, Cxt exte
*/
diffSchema(schemaOld: StorageSchema<any>, schemaNew: StorageSchema<any>): Plan;
}
type Plan = {
newTables: Record<string, {
attributes: Record<string, Attribute>;
}>;
newIndexes: Record<string, Index<any>[]>;
updatedTables: Record<string, {
attributes: Record<string, Attribute & {
isNew: boolean;
}>;
}>;
updatedIndexes: Record<string, Index<any>[]>;
};
export {};

View File

@ -81,6 +81,16 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
makeUpSchema() {
for (const entity in this.schema) {
const { attributes, indexes } = this.schema[entity];
// 非特殊索引自动添加 $$deleteAt$$ (by qcqcqc)
for (const index of indexes || []) {
if (index.config?.type) {
continue;
}
const indexAttrNames = index.attributes.map(attr => attr.name);
if (!indexAttrNames.includes('$$deleteAt$$')) {
index.attributes.push({ name: '$$deleteAt$$' });
}
}
const geoIndexes = [];
for (const attr in attributes) {
if (attributes[attr].type === 'geometry') {

View File

@ -16,10 +16,6 @@ export declare class PostgreSQLConnector {
exec(sql: string, txn?: string): Promise<[QueryResultRow[], QueryResult]>;
commitTransaction(txn: string): Promise<void>;
rollbackTransaction(txn: string): Promise<void>;
/**
* SQL
*/
execBatch(sqls: string[], txn?: string): Promise<void>;
/**
*
*/

View File

@ -133,16 +133,6 @@ class PostgreSQLConnector {
connection.release();
}
}
/**
* 执行多条 SQL 语句用于初始化等场景
*/
async execBatch(sqls, txn) {
for (const sql of sqls) {
if (sql.trim()) {
await this.exec(sql, txn);
}
}
}
/**
* 获取连接池状态
*/

View File

@ -7,7 +7,7 @@ import { PostgreSQLTranslator, PostgreSQLSelectOption, PostgreSQLOperateOption }
import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
import { CreateEntityOption } from '../types/Translator';
import { DbStore } from '../types/dbStore';
import { DbStore, Plan } from '../types/dbStore';
export declare class PostgreSQLStore<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>> extends CascadeStore<ED> implements DbStore<ED, Cxt> {
protected countAbjointRow<T extends keyof ED, OP extends SelectOption, Cxt extends SyncContext<ED>>(entity: T, selection: Pick<ED[T]['Selection'], 'filter' | 'count'>, context: Cxt, option: OP): number;
protected aggregateAbjointRowSync<T extends keyof ED, OP extends SelectOption, Cxt extends SyncContext<ED>>(entity: T, aggregation: ED[T]['Aggregation'], context: Cxt, option: OP): AggregationResult<ED[T]['Schema']>;
@ -35,4 +35,16 @@ export declare class PostgreSQLStore<ED extends EntityDict & BaseEntityDict, Cxt
connect(): Promise<void>;
disconnect(): Promise<void>;
initialize(option: CreateEntityOption): Promise<void>;
readSchema(): Promise<StorageSchema<ED>>;
/**
* dataSchemaschemaupgrade
* plan分为两阶段
*/
makeUpgradePlan(): Promise<Plan>;
/**
* schema的不同new对old的增量
* @param schemaOld
* @param schemaNew
*/
diffSchema(schemaOld: StorageSchema<any>, schemaNew: StorageSchema<any>): Plan;
}

View File

@ -393,5 +393,135 @@ class PostgreSQLStore extends CascadeStore_1.CascadeStore {
throw error;
}
}
// 从数据库中读取当前schema
readSchema() {
return this.translator.readSchema((sql) => this.connector.exec(sql));
}
/**
* 根据载入的dataSchema和数据库中原来的schema决定如何来upgrade
* 制订出来的plan分为两阶段增加阶段和削减阶段在两个阶段之间由用户来修正数据
*/
async makeUpgradePlan() {
const originSchema = await this.readSchema();
const plan = this.diffSchema(originSchema, this.translator.schema);
return plan;
}
/**
* 比较两个schema的不同这里计算的是new对old的增量
* @param schemaOld
* @param schemaNew
*/
diffSchema(schemaOld, schemaNew) {
const plan = {
newTables: {},
newIndexes: {},
updatedIndexes: {},
updatedTables: {},
};
for (const table in schemaNew) {
// PostgreSQL 表名区分大小写(使用双引号时)
if (schemaOld[table]) {
const { attributes, indexes } = schemaOld[table];
const { attributes: attributesNew, indexes: indexesNew } = schemaNew[table];
const assignToUpdateTables = (attr, isNew) => {
const skipAttrs = ['$$seq$$', '$$createAt$$', '$$updateAt$$', '$$deleteAt$$', 'id'];
if (skipAttrs.includes(attr)) {
return;
}
if (!plan.updatedTables[table]) {
plan.updatedTables[table] = {
attributes: {
[attr]: {
...attributesNew[attr],
isNew,
}
}
};
}
else {
plan.updatedTables[table].attributes[attr] = {
...attributesNew[attr],
isNew,
};
}
};
for (const attr in attributesNew) {
if (attributes[attr]) {
// 比较两次创建的属性定义是否一致
const sql1 = this.translator.translateAttributeDef(attr, attributesNew[attr]);
const sql2 = this.translator.translateAttributeDef(attr, attributes[attr]);
if (!this.translator.compareSql(sql1, sql2)) {
assignToUpdateTables(attr, false);
}
}
else {
assignToUpdateTables(attr, true);
}
}
if (indexesNew) {
const assignToIndexes = (index, isNew) => {
if (isNew) {
if (plan.newIndexes[table]) {
plan.newIndexes[table].push(index);
}
else {
plan.newIndexes[table] = [index];
}
}
else {
if (plan.updatedIndexes[table]) {
plan.updatedIndexes[table].push(index);
}
else {
plan.updatedIndexes[table] = [index];
}
}
};
const compareConfig = (config1, config2) => {
const unique1 = config1?.unique || false;
const unique2 = config2?.unique || false;
if (unique1 !== unique2) {
return false;
}
const type1 = config1?.type || 'btree';
const type2 = config2?.type || 'btree';
// tsConfig 比较
const tsConfig1 = config1?.tsConfig;
const tsConfig2 = config2?.tsConfig;
if (JSON.stringify(tsConfig1) !== JSON.stringify(tsConfig2)) {
return false;
}
return type1 === type2;
};
for (const index of indexesNew) {
const { name, config, attributes: indexAttrs } = index;
const origin = indexes?.find(ele => ele.name === name);
if (origin) {
if (JSON.stringify(indexAttrs) !== JSON.stringify(origin.attributes)) {
assignToIndexes(index, false);
}
else {
if (!compareConfig(config, origin.config)) {
assignToIndexes(index, false);
}
}
}
else {
assignToIndexes(index, true);
}
}
}
}
else {
plan.newTables[table] = {
attributes: schemaNew[table].attributes,
};
if (schemaNew[table].indexes) {
plan.newIndexes[table] = schemaNew[table].indexes;
}
}
}
return plan;
}
}
exports.PostgreSQLStore = PostgreSQLStore;

View File

@ -1,4 +1,4 @@
import { EntityDict, Q_FullTextValue, RefOrExpression, Ref, StorageSchema } from "oak-domain/lib/types";
import { EntityDict, Q_FullTextValue, RefOrExpression, Ref, StorageSchema, Attribute } from "oak-domain/lib/types";
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { DataType } from "oak-domain/lib/types/schema/DataTypes";
import { SqlOperateOption, SqlSelectOption, SqlTranslator } from "../sqlTranslator";
@ -80,4 +80,24 @@ export declare class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict
translateUpsert<T extends keyof ED>(entity: T, data: ED[T]['CreateMulti']['data'], conflictKeys: string[], updateAttrs?: string[]): string;
protected populateUpdateStmt(updateText: string, fromText: string, aliasDict: Record<string, string>, filterText: string, sorterText?: string, indexFrom?: number, count?: number, option?: PostgreSQLOperateOption): string;
protected populateRemoveStmt(updateText: string, fromText: string, aliasDict: Record<string, string>, filterText: string, sorterText?: string, indexFrom?: number, count?: number, option?: PostgreSQLOperateOption): string;
/**
* PostgreSQL Type oak populateDataTypeDef
* @param type PostgreSQL
*/
private reTranslateToAttribute;
/**
* PostgreSQL schema
*/
readSchema(execFn: (sql: string) => Promise<any>): Promise<StorageSchema<ED>>;
/**
* PostgreSQL DDL
* @param attr
* @param attrDef
*/
translateAttributeDef(attr: string, attrDef: Attribute): string;
/**
* SQL schema diff
*
*/
compareSql(sql1: string, sql2: string): boolean;
}

View File

@ -171,6 +171,16 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
makeUpSchema() {
for (const entity in this.schema) {
const { attributes, indexes } = this.schema[entity];
// 非特殊索引自动添加 $$deleteAt$$
for (const index of indexes || []) {
if (index.config?.type) {
continue;
}
const indexAttrNames = index.attributes.map(attr => attr.name);
if (!indexAttrNames.includes('$$deleteAt$$')) {
index.attributes.push({ name: '$$deleteAt$$' });
}
}
const geoIndexes = [];
for (const attr in attributes) {
if (attributes[attr].type === 'geometry') {
@ -741,6 +751,7 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
else if (typeof value === 'number') {
return `${value}`;
}
(0, assert_1.default)(typeof value === 'string', 'Invalid date/time value');
return `'${(new Date(value)).valueOf()}'`;
}
case 'object':
@ -967,12 +978,8 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
}
indexSql += '(';
const indexColumns = [];
let includeDeleteAt = false;
for (const indexAttr of indexAttrs) {
const { name: attrName, direction } = indexAttr;
if (attrName === '$$deleteAt$$') {
includeDeleteAt = true;
}
if (indexType === 'fulltext') {
// 全文索引:使用 to_tsvector
indexColumns.push(`to_tsvector('${tsLang}', COALESCE("${attrName}", ''))`);
@ -986,10 +993,6 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
indexColumns.push(col);
}
}
// 非特殊索引自动包含 deleteAt
if (!includeDeleteAt && !indexType) {
indexColumns.push('"$$deleteAt$$"');
}
indexSql += indexColumns.join(', ');
indexSql += ');';
sqls.push(indexSql);
@ -1766,5 +1769,371 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
// 这个方法不应该被直接调用了因为translateRemove已经重写
throw new Error('populateRemoveStmt should not be called directly in PostgreSQL. Use translateRemove instead.');
}
/**
* PostgreSQL 返回的 Type 回译成 oak 的类型 populateDataTypeDef 的反函数
* @param type PostgreSQL 类型字符串
*/
reTranslateToAttribute(type) {
// 处理带长度的类型character varying(255), character(10)
const varcharMatch = /^character varying\((\d+)\)$/.exec(type);
if (varcharMatch) {
return {
type: 'varchar',
params: {
length: parseInt(varcharMatch[1], 10),
}
};
}
const charMatch = /^character\((\d+)\)$/.exec(type);
if (charMatch) {
return {
type: 'char',
params: {
length: parseInt(charMatch[1], 10),
}
};
}
// 处理带精度和小数位的类型numeric(10,2)
const numericWithScaleMatch = /^numeric\((\d+),(\d+)\)$/.exec(type);
if (numericWithScaleMatch) {
return {
type: 'decimal',
params: {
precision: parseInt(numericWithScaleMatch[1], 10),
scale: parseInt(numericWithScaleMatch[2], 10),
},
};
}
// 处理只带精度的类型numeric(10), timestamp(6)
const numericMatch = /^numeric\((\d+)\)$/.exec(type);
if (numericMatch) {
return {
type: 'decimal',
params: {
precision: parseInt(numericMatch[1], 10),
scale: 0,
},
};
}
const timestampMatch = /^timestamp\((\d+)\) without time zone$/.exec(type);
if (timestampMatch) {
return {
type: 'timestamp',
params: {
precision: parseInt(timestampMatch[1], 10),
},
};
}
const timeMatch = /^time\((\d+)\) without time zone$/.exec(type);
if (timeMatch) {
return {
type: 'time',
params: {
precision: parseInt(timeMatch[1], 10),
},
};
}
// PostgreSQL 类型映射到 oak 类型
const typeMap = {
'bigint': 'bigint',
'integer': 'integer',
'smallint': 'smallint',
'real': 'real',
'double precision': 'double precision',
'boolean': 'boolean',
'text': 'text',
'jsonb': 'object', // 框架使用 jsonb 存储 object/array
'json': 'object',
'bytea': 'bytea',
'character varying': 'varchar',
'character': 'char',
'timestamp without time zone': 'timestamp',
'time without time zone': 'time',
'date': 'date',
'uuid': 'uuid',
'geometry': 'geometry',
};
const mappedType = typeMap[type];
if (mappedType) {
return { type: mappedType };
}
// 如果是用户定义的枚举类型,返回 enum具体值需要额外查询
// 这里先返回基础类型,枚举值在 readSchema 中单独处理
return { type: type };
}
/**
* PostgreSQL 数据库读取当前的 schema 结构
*/
async readSchema(execFn) {
const result = {};
// 1. 获取所有表
const tablesSql = `
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
`;
const [tablesResult] = await execFn(tablesSql);
for (const tableRow of tablesResult) {
const tableName = tableRow.tablename;
// 2. 获取表的列信息
const columnsSql = `
SELECT
column_name,
data_type,
character_maximum_length,
numeric_precision,
numeric_scale,
is_nullable,
column_default,
udt_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = '${tableName}'
ORDER BY ordinal_position;
`;
const [columnsResult] = await execFn(columnsSql);
const attributes = {};
for (const col of columnsResult) {
const { column_name: colName, data_type: dataType, character_maximum_length: maxLength, numeric_precision: precision, numeric_scale: scale, is_nullable: isNullable, column_default: defaultValue, udt_name: udtName, } = col;
let attr;
// 处理用户定义类型(枚举)
if (dataType === 'USER-DEFINED') {
// 查询枚举值
const enumSql = `
SELECT e.enumlabel
FROM pg_type t
JOIN pg_enum e ON t.oid = e.enumtypid
WHERE t.typname = '${udtName}'
ORDER BY e.enumsortorder;
`;
const [enumResult] = await execFn(enumSql);
const enumeration = enumResult.map((r) => r.enumlabel);
attr = {
type: 'enum',
enumeration,
};
}
else {
// 构建完整的类型字符串
let fullType = dataType;
if (maxLength) {
fullType = `${dataType}(${maxLength})`;
}
else if (precision !== null && scale !== null) {
fullType = `${dataType}(${precision},${scale})`;
}
else if (precision !== null) {
fullType = `${dataType}(${precision})`;
}
attr = this.reTranslateToAttribute(fullType);
}
// 处理约束
attr.notNull = isNullable === 'NO';
// 处理默认值(简化处理,复杂的默认值可能需要更精细的解析)
if (defaultValue && !defaultValue.includes('nextval')) {
// 跳过序列默认值
// 简单处理:移除类型转换
let cleanDefault = defaultValue.replace(/::[a-z]+/gi, '').replace(/'/g, '');
if (cleanDefault === 'true') {
attr.default = true;
}
else if (cleanDefault === 'false') {
attr.default = false;
}
else if (!isNaN(Number(cleanDefault))) {
attr.default = Number(cleanDefault);
}
else {
attr.default = cleanDefault;
}
}
// 检查是否是序列列IDENTITY
if (colName === '$$seq$$' || (defaultValue && defaultValue.includes('nextval'))) {
attr.sequenceStart = 10000; // 默认起始值
}
attributes[colName] = attr;
}
// 3. 获取索引信息
const indexesSql = `
SELECT
i.relname as index_name,
ix.indisunique as is_unique,
am.amname as index_type,
pg_get_indexdef(ix.indexrelid) as index_def
FROM pg_class t
JOIN pg_index ix ON t.oid = ix.indrelid
JOIN pg_class i ON i.oid = ix.indexrelid
JOIN pg_am am ON i.relam = am.oid
WHERE t.relname = '${tableName}'
AND t.relkind = 'r'
AND i.relname NOT LIKE '%_pkey'
AND NOT ix.indisprimary
ORDER BY i.relname;
`;
const [indexesResult] = await execFn(indexesSql);
if (indexesResult.length > 0) {
const indexes = [];
for (const row of indexesResult) {
const { index_name: indexName, is_unique: isUnique, index_type: indexType, index_def: indexDef } = row;
// 解析索引定义以获取列名和配置
const index = {
name: indexName,
attributes: [],
};
// 解析索引定义字符串
// 示例: CREATE INDEX "user_index_fulltext_chinese" ON public."user" USING gin (to_tsvector('chinese'::regconfig, (COALESCE(name, ''::text) || ' '::text) || COALESCE(nickname, ''::text)))
if (indexType === 'gin' && indexDef.includes('to_tsvector')) {
// 全文索引
index.config = { type: 'fulltext' };
// 提取 tsConfig
const tsConfigMatch = indexDef.match(/to_tsvector\('([^']+)'/);
if (tsConfigMatch) {
const tsConfig = tsConfigMatch[1];
index.config.tsConfig = tsConfig;
}
// 提取列名(从 COALESCE 中)
const columnMatches = indexDef.matchAll(/COALESCE\("?([^",\s]+)"?/g);
const columns = Array.from(columnMatches, m => m[1]);
index.attributes = columns.map(col => ({ name: col }));
// 处理多语言索引的情况:移除语言后缀
// 例如: user_index_fulltext_chinese -> index_fulltext
const nameParts = indexName.split('_');
if (nameParts.length > 2) {
const possibleLang = nameParts[nameParts.length - 1];
// 如果最后一部分是语言代码,移除它
if (['chinese', 'english', 'simple', 'german', 'french', 'spanish', 'russian', 'japanese'].includes(possibleLang)) {
index.name = nameParts.slice(0, -1).join('_');
}
}
}
else if (indexType === 'gist') {
// 空间索引
index.config = { type: 'spatial' };
// 提取列名
const columnMatch = indexDef.match(/\(([^)]+)\)/);
if (columnMatch) {
const columns = columnMatch[1].split(',').map(c => c.trim().replace(/"/g, ''));
index.attributes = columns.map(col => ({ name: col }));
}
}
else if (indexType === 'hash') {
// 哈希索引
index.config = { type: 'hash' };
// 提取列名
const columnMatch = indexDef.match(/\(([^)]+)\)/);
if (columnMatch) {
const columns = columnMatch[1].split(',').map(c => c.trim().replace(/"/g, ''));
index.attributes = columns.map(col => ({ name: col }));
}
}
else {
// B-tree 索引(默认)
// 提取列名和排序方向
const columnMatch = indexDef.match(/\(([^)]+)\)/);
if (columnMatch) {
const columnDefs = columnMatch[1].split(',');
index.attributes = columnDefs.map(colDef => {
const trimmed = colDef.trim().replace(/"/g, '');
const parts = trimmed.split(/\s+/);
const attr = { name: parts[0] };
// 检查排序方向
if (parts.includes('DESC')) {
attr.direction = 'DESC';
}
else if (parts.includes('ASC')) {
attr.direction = 'ASC';
}
return attr;
});
}
// 如果是唯一索引
if (isUnique) {
index.config = { unique: true };
}
}
// 移除表名前缀(如果存在)
// 例如: user_index_fulltext -> index_fulltext
if (index.name.startsWith(`${tableName}_`)) {
index.name = index.name.substring(tableName.length + 1);
}
indexes.push(index);
}
Object.assign(result, {
[tableName]: {
attributes,
indexes,
}
});
}
else {
Object.assign(result, {
[tableName]: {
attributes,
}
});
}
}
return result;
}
/**
* 将属性定义转换为 PostgreSQL DDL 语句
* @param attr 属性名
* @param attrDef 属性定义
*/
translateAttributeDef(attr, attrDef) {
let sql = `"${attr}" `;
const { type, params, default: defaultValue, unique, notNull, sequenceStart, enumeration, } = attrDef;
// 处理序列类型IDENTITY
if (typeof sequenceStart === 'number') {
sql += `bigint GENERATED BY DEFAULT AS IDENTITY (START WITH ${sequenceStart}) UNIQUE`;
return sql;
}
// 处理枚举类型
if (type === 'enum') {
(0, assert_1.default)(enumeration, 'Enum type requires enumeration values');
// 注意:这里返回的是占位符,实际的枚举类型名在 translateCreateEntity 中确定
// 为了比较一致性,我们使用枚举值的字符串表示
sql += `enum(${enumeration.map(v => `'${v}'`).join(',')})`;
}
else {
sql += this.populateDataTypeDef(type, params, enumeration);
}
// NOT NULL 约束
if (notNull || type === 'geometry') {
sql += ' NOT NULL';
}
// UNIQUE 约束
if (unique) {
sql += ' UNIQUE';
}
// 默认值
if (defaultValue !== undefined && !sequenceStart) {
(0, assert_1.default)(type !== 'ref', 'ref type should not have default value');
sql += ` DEFAULT ${this.translateAttrValue(type, defaultValue)}`;
}
// 主键
if (attr === 'id') {
sql += ' PRIMARY KEY';
}
return sql;
}
/**
* 比较两个 SQL 语句是否等价用于 schema diff
* 忽略空格大小写等格式差异
*/
compareSql(sql1, sql2) {
// 标准化 SQL移除多余空格统一大小写
const normalize = (sql) => {
return sql
.replace(/\s+/g, ' ') // 多个空格合并为一个
.replace(/\(\s+/g, '(') // 移除括号后的空格
.replace(/\s+\)/g, ')') // 移除括号前的空格
.replace(/,\s+/g, ',') // 移除逗号后的空格
.trim()
.toLowerCase();
};
return normalize(sql1) === normalize(sql2);
}
}
exports.PostgreSQLTranslator = PostgreSQLTranslator;

View File

@ -1,8 +1,32 @@
import { EntityDict } from "oak-domain/lib/base-app-domain";
import { Attribute, EntityDict, Index, OperateOption, OperationResult, StorageSchema, TxnOption } from 'oak-domain/lib/types';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { AsyncContext, AsyncRowStore } from "oak-domain/lib/store/AsyncRowStore";
import { CreateEntityOption } from "./Translator";
export interface DbStore<ED extends EntityDict, Cxt extends AsyncContext<ED>> extends AsyncRowStore<ED, Cxt> {
import { AggregationResult, SelectOption } from "oak-domain/lib/types";
export type Plan = {
newTables: Record<string, {
attributes: Record<string, Attribute>;
}>;
newIndexes: Record<string, Index<any>[]>;
updatedTables: Record<string, {
attributes: Record<string, Attribute & {
isNew: boolean;
}>;
}>;
updatedIndexes: Record<string, Index<any>[]>;
};
export interface DbStore<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>> extends AsyncRowStore<ED, Cxt> {
connect: () => Promise<void>;
disconnect: () => Promise<void>;
initialize(options: CreateEntityOption): Promise<void>;
aggregate<T extends keyof ED, OP extends SelectOption>(entity: T, aggregation: ED[T]['Aggregation'], context: Cxt, option: OP): Promise<AggregationResult<ED[T]['Schema']>>;
operate<T extends keyof ED>(entity: T, operation: ED[T]['Operation'], context: Cxt, option: OperateOption): Promise<OperationResult<ED>>;
select<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], context: Cxt, option: SelectOption): Promise<Partial<ED[T]['Schema']>[]>;
count<T extends keyof ED>(entity: T, selection: Pick<ED[T]['Selection'], 'filter' | 'count'>, context: Cxt, option: SelectOption): Promise<number>;
begin(option?: TxnOption): Promise<string>;
commit(txnId: string): Promise<void>;
rollback(txnId: string): Promise<void>;
readSchema(): Promise<StorageSchema<ED>>;
makeUpgradePlan(): Promise<Plan>;
diffSchema(schemaOld: StorageSchema<any>, schemaNew: StorageSchema<any>): Plan;
}

View File

@ -11,7 +11,7 @@ import { AsyncContext, AsyncRowStore } from 'oak-domain/lib/store/AsyncRowStore'
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
import { CreateEntityOption } from '../types/Translator';
import { FieldPacket, ResultSetHeader, RowDataPacket } from 'mysql2';
import { DbStore } from '../types/dbStore';
import { DbStore, Plan } from '../types/dbStore';
function convertGeoTextToObject(geoText: string): Geo {
@ -466,16 +466,4 @@ export class MysqlStore<ED extends EntityDict & BaseEntityDict, Cxt extends Asyn
return plan;
}
}
type Plan = {
newTables: Record<string, {
attributes: Record<string, Attribute>;
}>;
newIndexes: Record<string, Index<any>[]>;
updatedTables: Record<string, {
attributes: Record<string, Attribute & { isNew: boolean }>;
}>;
updatedIndexes: Record<string, Index<any>[]>;
};
}

View File

@ -109,6 +109,16 @@ export class MySqlTranslator<ED extends EntityDict & BaseEntityDict> extends Sql
private makeUpSchema() {
for (const entity in this.schema) {
const { attributes, indexes } = this.schema[entity];
// 非特殊索引自动添加 $$deleteAt$$ (by qcqcqc)
for (const index of indexes || []) {
if (index.config?.type) {
continue;
}
const indexAttrNames = index.attributes.map(attr => attr.name);
if (!indexAttrNames.includes('$$deleteAt$$')) {
index.attributes.push({ name: '$$deleteAt$$' });
}
}
const geoIndexes: Index<ED[keyof ED]['OpSchema']>[] = [];
for (const attr in attributes) {
if (attributes[attr].type === 'geometry') {

View File

@ -155,17 +155,6 @@ export class PostgreSQLConnector {
}
}
/**
* SQL
*/
async execBatch(sqls: string[], txn?: string): Promise<void> {
for (const sql of sqls) {
if (sql.trim()) {
await this.exec(sql, txn);
}
}
}
/**
*
*/

View File

@ -6,7 +6,10 @@ import {
StorageSchema,
SelectOption,
AggregationResult,
Geo
Geo,
IndexConfig,
Index,
Attribute
} from 'oak-domain/lib/types';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { CascadeStore } from 'oak-domain/lib/store/CascadeStore';
@ -20,7 +23,7 @@ import { AsyncContext, AsyncRowStore } from 'oak-domain/lib/store/AsyncRowStore'
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
import { CreateEntityOption } from '../types/Translator';
import { QueryResult } from 'pg';
import { DbStore } from '../types/dbStore';
import { DbStore, Plan } from '../types/dbStore';
const ToNumberAttrs = new Set([
'$$seq$$',
@ -511,4 +514,138 @@ export class PostgreSQLStore<
throw error;
}
}
// 从数据库中读取当前schema
readSchema() {
return this.translator.readSchema((sql) => this.connector.exec(sql));
}
/**
* dataSchemaschemaupgrade
* plan分为两阶段
*/
async makeUpgradePlan() {
const originSchema = await this.readSchema();
const plan = this.diffSchema(originSchema, this.translator.schema);
return plan;
}
/**
* schema的不同new对old的增量
* @param schemaOld
* @param schemaNew
*/
diffSchema(schemaOld: StorageSchema<any>, schemaNew: StorageSchema<any>) {
const plan: Plan = {
newTables: {},
newIndexes: {},
updatedIndexes: {},
updatedTables: {},
};
for (const table in schemaNew) {
// PostgreSQL 表名区分大小写(使用双引号时)
if (schemaOld[table]) {
const { attributes, indexes } = schemaOld[table];
const { attributes: attributesNew, indexes: indexesNew } = schemaNew[table];
const assignToUpdateTables = (attr: string, isNew: boolean) => {
const skipAttrs = ['$$seq$$', '$$createAt$$', '$$updateAt$$', '$$deleteAt$$', 'id'];
if (skipAttrs.includes(attr)) {
return;
}
if (!plan.updatedTables[table]) {
plan.updatedTables[table] = {
attributes: {
[attr]: {
...attributesNew[attr],
isNew,
}
}
};
} else {
plan.updatedTables[table].attributes[attr] = {
...attributesNew[attr],
isNew,
};
}
};
for (const attr in attributesNew) {
if (attributes[attr]) {
// 比较两次创建的属性定义是否一致
const sql1 = this.translator.translateAttributeDef(attr, attributesNew[attr]);
const sql2 = this.translator.translateAttributeDef(attr, attributes[attr]);
if (!this.translator.compareSql(sql1, sql2)) {
assignToUpdateTables(attr, false);
}
} else {
assignToUpdateTables(attr, true);
}
}
if (indexesNew) {
const assignToIndexes = (index: Index<any>, isNew: boolean) => {
if (isNew) {
if (plan.newIndexes[table]) {
plan.newIndexes[table].push(index);
} else {
plan.newIndexes[table] = [index];
}
} else {
if (plan.updatedIndexes[table]) {
plan.updatedIndexes[table].push(index);
} else {
plan.updatedIndexes[table] = [index];
}
}
};
const compareConfig = (config1?: IndexConfig, config2?: IndexConfig) => {
const unique1 = config1?.unique || false;
const unique2 = config2?.unique || false;
if (unique1 !== unique2) {
return false;
}
const type1 = config1?.type || 'btree';
const type2 = config2?.type || 'btree';
// tsConfig 比较
const tsConfig1 = config1?.tsConfig;
const tsConfig2 = config2?.tsConfig;
if (JSON.stringify(tsConfig1) !== JSON.stringify(tsConfig2)) {
return false;
}
return type1 === type2;
};
for (const index of indexesNew) {
const { name, config, attributes: indexAttrs } = index;
const origin = indexes?.find(ele => ele.name === name);
if (origin) {
if (JSON.stringify(indexAttrs) !== JSON.stringify(origin.attributes)) {
assignToIndexes(index, false);
} else {
if (!compareConfig(config, origin.config)) {
assignToIndexes(index, false);
}
}
} else {
assignToIndexes(index, true);
}
}
}
} else {
plan.newTables[table] = {
attributes: schemaNew[table].attributes,
};
if (schemaNew[table].indexes) {
plan.newIndexes[table] = schemaNew[table].indexes!;
}
}
}
return plan;
}
}

View File

@ -1,7 +1,7 @@
import assert from 'assert';
import { format } from 'util';
import { assign, difference } from 'lodash';
import { EntityDict, Geo, Q_FullTextValue, RefOrExpression, Ref, StorageSchema, Index, RefAttr, DeleteAtAttribute, TriggerDataAttribute, TriggerUuidAttribute, UpdateAtAttribute } from "oak-domain/lib/types";
import { EntityDict, Geo, Q_FullTextValue, RefOrExpression, Ref, StorageSchema, Index, RefAttr, DeleteAtAttribute, TriggerDataAttribute, TriggerUuidAttribute, UpdateAtAttribute, Attribute, Attributes } from "oak-domain/lib/types";
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { DataType, DataTypeParams } from "oak-domain/lib/types/schema/DataTypes";
import { SqlOperateOption, SqlSelectOption, SqlTranslator } from "../sqlTranslator";
@ -210,6 +210,16 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
private makeUpSchema() {
for (const entity in this.schema) {
const { attributes, indexes } = this.schema[entity];
// 非特殊索引自动添加 $$deleteAt$$
for (const index of indexes || []) {
if (index.config?.type) {
continue;
}
const indexAttrNames = index.attributes.map(attr => attr.name);
if (!indexAttrNames.includes('$$deleteAt$$')) {
index.attributes.push({ name: '$$deleteAt$$' });
}
}
const geoIndexes: Index<ED[keyof ED]['OpSchema']>[] = [];
for (const attr in attributes) {
if (attributes[attr].type === 'geometry') {
@ -796,6 +806,7 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
else if (typeof value === 'number') {
return `${value}`;
}
assert(typeof value === 'string', 'Invalid date/time value');
return `'${(new Date(value)).valueOf()}'`;
}
case 'object':
@ -1066,15 +1077,10 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
indexSql += '(';
const indexColumns: string[] = [];
let includeDeleteAt = false;
for (const indexAttr of indexAttrs) {
const { name: attrName, direction } = indexAttr;
if (attrName === '$$deleteAt$$') {
includeDeleteAt = true;
}
if (indexType === 'fulltext') {
// 全文索引:使用 to_tsvector
indexColumns.push(`to_tsvector('${tsLang}', COALESCE("${attrName as string}", ''))`);
@ -1088,11 +1094,6 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
}
}
// 非特殊索引自动包含 deleteAt
if (!includeDeleteAt && !indexType) {
indexColumns.push('"$$deleteAt$$"');
}
indexSql += indexColumns.join(', ');
indexSql += ');';
@ -2038,4 +2039,431 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
// 这个方法不应该被直接调用了因为translateRemove已经重写
throw new Error('populateRemoveStmt should not be called directly in PostgreSQL. Use translateRemove instead.');
}
/**
* PostgreSQL Type oak populateDataTypeDef
* @param type PostgreSQL
*/
private reTranslateToAttribute(type: string): Attribute {
// 处理带长度的类型character varying(255), character(10)
const varcharMatch = /^character varying\((\d+)\)$/.exec(type);
if (varcharMatch) {
return {
type: 'varchar',
params: {
length: parseInt(varcharMatch[1], 10),
}
};
}
const charMatch = /^character\((\d+)\)$/.exec(type);
if (charMatch) {
return {
type: 'char',
params: {
length: parseInt(charMatch[1], 10),
}
};
}
// 处理带精度和小数位的类型numeric(10,2)
const numericWithScaleMatch = /^numeric\((\d+),(\d+)\)$/.exec(type);
if (numericWithScaleMatch) {
return {
type: 'decimal',
params: {
precision: parseInt(numericWithScaleMatch[1], 10),
scale: parseInt(numericWithScaleMatch[2], 10),
},
};
}
// 处理只带精度的类型numeric(10), timestamp(6)
const numericMatch = /^numeric\((\d+)\)$/.exec(type);
if (numericMatch) {
return {
type: 'decimal',
params: {
precision: parseInt(numericMatch[1], 10),
scale: 0,
},
};
}
const timestampMatch = /^timestamp\((\d+)\) without time zone$/.exec(type);
if (timestampMatch) {
return {
type: 'timestamp',
params: {
precision: parseInt(timestampMatch[1], 10),
},
};
}
const timeMatch = /^time\((\d+)\) without time zone$/.exec(type);
if (timeMatch) {
return {
type: 'time',
params: {
precision: parseInt(timeMatch[1], 10),
},
};
}
// PostgreSQL 类型映射到 oak 类型
const typeMap: Record<string, DataType> = {
'bigint': 'bigint',
'integer': 'integer',
'smallint': 'smallint',
'real': 'real',
'double precision': 'double precision',
'boolean': 'boolean',
'text': 'text',
'jsonb': 'object', // 框架使用 jsonb 存储 object/array
'json': 'object',
'bytea': 'bytea',
'character varying': 'varchar',
'character': 'char',
'timestamp without time zone': 'timestamp',
'time without time zone': 'time',
'date': 'date',
'uuid': 'uuid',
'geometry': 'geometry',
};
const mappedType = typeMap[type];
if (mappedType) {
return { type: mappedType };
}
// 如果是用户定义的枚举类型,返回 enum具体值需要额外查询
// 这里先返回基础类型,枚举值在 readSchema 中单独处理
return { type: type as DataType };
}
/**
* PostgreSQL schema
*/
async readSchema(execFn: (sql: string) => Promise<any>): Promise<StorageSchema<ED>> {
const result: StorageSchema<ED> = {} as StorageSchema<ED>;
// 1. 获取所有表
const tablesSql = `
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
`;
const [tablesResult] = await execFn(tablesSql);
for (const tableRow of tablesResult) {
const tableName = tableRow.tablename;
// 2. 获取表的列信息
const columnsSql = `
SELECT
column_name,
data_type,
character_maximum_length,
numeric_precision,
numeric_scale,
is_nullable,
column_default,
udt_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = '${tableName}'
ORDER BY ordinal_position;
`;
const [columnsResult] = await execFn(columnsSql);
const attributes: Attributes<any> = {};
for (const col of columnsResult) {
const {
column_name: colName,
data_type: dataType,
character_maximum_length: maxLength,
numeric_precision: precision,
numeric_scale: scale,
is_nullable: isNullable,
column_default: defaultValue,
udt_name: udtName,
} = col;
let attr: Attribute;
// 处理用户定义类型(枚举)
if (dataType === 'USER-DEFINED') {
// 查询枚举值
const enumSql = `
SELECT e.enumlabel
FROM pg_type t
JOIN pg_enum e ON t.oid = e.enumtypid
WHERE t.typname = '${udtName}'
ORDER BY e.enumsortorder;
`;
const [enumResult] = await execFn(enumSql);
const enumeration = enumResult.map((r: any) => r.enumlabel);
attr = {
type: 'enum',
enumeration,
};
} else {
// 构建完整的类型字符串
let fullType = dataType;
if (maxLength) {
fullType = `${dataType}(${maxLength})`;
} else if (precision !== null && scale !== null) {
fullType = `${dataType}(${precision},${scale})`;
} else if (precision !== null) {
fullType = `${dataType}(${precision})`;
}
attr = this.reTranslateToAttribute(fullType);
}
// 处理约束
attr.notNull = isNullable === 'NO';
// 处理默认值(简化处理,复杂的默认值可能需要更精细的解析)
if (defaultValue && !defaultValue.includes('nextval')) {
// 跳过序列默认值
// 简单处理:移除类型转换
let cleanDefault = defaultValue.replace(/::[a-z]+/gi, '').replace(/'/g, '');
if (cleanDefault === 'true') {
attr.default = true;
} else if (cleanDefault === 'false') {
attr.default = false;
} else if (!isNaN(Number(cleanDefault))) {
attr.default = Number(cleanDefault);
} else {
attr.default = cleanDefault;
}
}
// 检查是否是序列列IDENTITY
if (colName === '$$seq$$' || (defaultValue && defaultValue.includes('nextval'))) {
attr.sequenceStart = 10000; // 默认起始值
}
attributes[colName] = attr;
}
// 3. 获取索引信息
const indexesSql = `
SELECT
i.relname as index_name,
ix.indisunique as is_unique,
am.amname as index_type,
pg_get_indexdef(ix.indexrelid) as index_def
FROM pg_class t
JOIN pg_index ix ON t.oid = ix.indrelid
JOIN pg_class i ON i.oid = ix.indexrelid
JOIN pg_am am ON i.relam = am.oid
WHERE t.relname = '${tableName}'
AND t.relkind = 'r'
AND i.relname NOT LIKE '%_pkey'
AND NOT ix.indisprimary
ORDER BY i.relname;
`;
const [indexesResult] = await execFn(indexesSql) as [{
index_name: string;
is_unique: boolean;
index_type: string;
index_def: string;
}[]]
if (indexesResult.length > 0) {
const indexes: Index<any>[] = [];
for (const row of indexesResult) {
const { index_name: indexName, is_unique: isUnique, index_type: indexType, index_def: indexDef } = row;
// 解析索引定义以获取列名和配置
const index: Index<any> = {
name: indexName,
attributes: [],
};
// 解析索引定义字符串
// 示例: CREATE INDEX "user_index_fulltext_chinese" ON public."user" USING gin (to_tsvector('chinese'::regconfig, (COALESCE(name, ''::text) || ' '::text) || COALESCE(nickname, ''::text)))
if (indexType === 'gin' && indexDef.includes('to_tsvector')) {
// 全文索引
index.config = { type: 'fulltext' };
// 提取 tsConfig
const tsConfigMatch = indexDef.match(/to_tsvector\('([^']+)'/);
if (tsConfigMatch) {
const tsConfig = tsConfigMatch[1];
index.config.tsConfig = tsConfig;
}
// 提取列名(从 COALESCE 中)
const columnMatches = indexDef.matchAll(/COALESCE\("?([^",\s]+)"?/g);
const columns = Array.from(columnMatches, m => m[1]);
index.attributes = columns.map(col => ({ name: col }));
// 处理多语言索引的情况:移除语言后缀
// 例如: user_index_fulltext_chinese -> index_fulltext
const nameParts = indexName.split('_');
if (nameParts.length > 2) {
const possibleLang = nameParts[nameParts.length - 1];
// 如果最后一部分是语言代码,移除它
if (['chinese', 'english', 'simple', 'german', 'french', 'spanish', 'russian', 'japanese'].includes(possibleLang)) {
index.name = nameParts.slice(0, -1).join('_');
}
}
} else if (indexType === 'gist') {
// 空间索引
index.config = { type: 'spatial' };
// 提取列名
const columnMatch = indexDef.match(/\(([^)]+)\)/);
if (columnMatch) {
const columns = columnMatch[1].split(',').map(c => c.trim().replace(/"/g, ''));
index.attributes = columns.map(col => ({ name: col }));
}
} else if (indexType === 'hash') {
// 哈希索引
index.config = { type: 'hash' };
// 提取列名
const columnMatch = indexDef.match(/\(([^)]+)\)/);
if (columnMatch) {
const columns = columnMatch[1].split(',').map(c => c.trim().replace(/"/g, ''));
index.attributes = columns.map(col => ({ name: col }));
}
} else {
// B-tree 索引(默认)
// 提取列名和排序方向
const columnMatch = indexDef.match(/\(([^)]+)\)/);
if (columnMatch) {
const columnDefs = columnMatch[1].split(',');
index.attributes = columnDefs.map(colDef => {
const trimmed = colDef.trim().replace(/"/g, '');
const parts = trimmed.split(/\s+/);
const attr: any = { name: parts[0] };
// 检查排序方向
if (parts.includes('DESC')) {
attr.direction = 'DESC';
} else if (parts.includes('ASC')) {
attr.direction = 'ASC';
}
return attr;
});
}
// 如果是唯一索引
if (isUnique) {
index.config = { unique: true };
}
}
// 移除表名前缀(如果存在)
// 例如: user_index_fulltext -> index_fulltext
if (index.name.startsWith(`${tableName}_`)) {
index.name = index.name.substring(tableName.length + 1);
}
indexes.push(index);
}
Object.assign(result, {
[tableName]: {
attributes,
indexes,
}
});
} else {
Object.assign(result, {
[tableName]: {
attributes,
}
});
}
}
return result;
}
/**
* PostgreSQL DDL
* @param attr
* @param attrDef
*/
translateAttributeDef(attr: string, attrDef: Attribute): string {
let sql = `"${attr}" `;
const {
type,
params,
default: defaultValue,
unique,
notNull,
sequenceStart,
enumeration,
} = attrDef;
// 处理序列类型IDENTITY
if (typeof sequenceStart === 'number') {
sql += `bigint GENERATED BY DEFAULT AS IDENTITY (START WITH ${sequenceStart}) UNIQUE`;
return sql;
}
// 处理枚举类型
if (type === 'enum') {
assert(enumeration, 'Enum type requires enumeration values');
// 注意:这里返回的是占位符,实际的枚举类型名在 translateCreateEntity 中确定
// 为了比较一致性,我们使用枚举值的字符串表示
sql += `enum(${enumeration.map(v => `'${v}'`).join(',')})`;
} else {
sql += this.populateDataTypeDef(type, params, enumeration);
}
// NOT NULL 约束
if (notNull || type === 'geometry') {
sql += ' NOT NULL';
}
// UNIQUE 约束
if (unique) {
sql += ' UNIQUE';
}
// 默认值
if (defaultValue !== undefined && !sequenceStart) {
assert(type !== 'ref', 'ref type should not have default value');
sql += ` DEFAULT ${this.translateAttrValue(type, defaultValue)}`;
}
// 主键
if (attr === 'id') {
sql += ' PRIMARY KEY';
}
return sql;
}
/**
* SQL schema diff
*
*/
compareSql(sql1: string, sql2: string): boolean {
// 标准化 SQL移除多余空格统一大小写
const normalize = (sql: string): string => {
return sql
.replace(/\s+/g, ' ') // 多个空格合并为一个
.replace(/\(\s+/g, '(') // 移除括号后的空格
.replace(/\s+\)/g, ')') // 移除括号前的空格
.replace(/,\s+/g, ',') // 移除逗号后的空格
.trim()
.toLowerCase();
};
return normalize(sql1) === normalize(sql2);
}
}

View File

@ -1,9 +1,60 @@
import { EntityDict } from "oak-domain/lib/base-app-domain";
import {
Attribute,
EntityDict,
Index,
OperateOption,
OperationResult,
StorageSchema,
TxnOption,
} from 'oak-domain/lib/types';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { AsyncContext,AsyncRowStore } from "oak-domain/lib/store/AsyncRowStore";
import { CreateEntityOption } from "./Translator";
import { AggregationResult, SelectOption } from "oak-domain/lib/types";
export interface DbStore<ED extends EntityDict, Cxt extends AsyncContext<ED>> extends AsyncRowStore<ED, Cxt> {
export type Plan = {
newTables: Record<string, {
attributes: Record<string, Attribute>;
}>;
newIndexes: Record<string, Index<any>[]>;
updatedTables: Record<string, {
attributes: Record<string, Attribute & { isNew: boolean }>;
}>;
updatedIndexes: Record<string, Index<any>[]>;
};
export interface DbStore<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>> extends AsyncRowStore<ED, Cxt> {
connect: () => Promise<void>;
disconnect: () => Promise<void>;
initialize(options: CreateEntityOption): Promise<void>;
aggregate<T extends keyof ED, OP extends SelectOption>(
entity: T,
aggregation: ED[T]['Aggregation'],
context: Cxt,
option: OP
): Promise<AggregationResult<ED[T]['Schema']>>;
operate<T extends keyof ED>(
entity: T,
operation: ED[T]['Operation'],
context: Cxt,
option: OperateOption
): Promise<OperationResult<ED>>;
select<T extends keyof ED>(
entity: T,
selection: ED[T]['Selection'],
context: Cxt,
option: SelectOption
): Promise<Partial<ED[T]['Schema']>[]>;
count<T extends keyof ED>(
entity: T,
selection: Pick<ED[T]['Selection'], 'filter' | 'count'>,
context: Cxt,
option: SelectOption
): Promise<number>;
begin(option?: TxnOption): Promise<string>;
commit(txnId: string): Promise<void>;
rollback(txnId: string): Promise<void>;
readSchema(): Promise<StorageSchema<ED>>;
makeUpgradePlan(): Promise<Plan>;
diffSchema(schemaOld: StorageSchema<any>, schemaNew: StorageSchema<any>): Plan;
};