创建数据库方言
创建数据库方言
添加数据库方言时,从数据库类型、SQL 渲染、表结构语句、运行时注册和验证用例五个入口完成。
内置方言和用户可见行为示例请参考
📚数据库方言支持。
1. 识别数据库类型
KronosJdbcWrapper 会读取 JDBC metadata,并通过 DBType.fromName(...) 得到运行时数据库类型。
kotlin
enum class DBType {
Mysql,
Oracle,
Postgres,
Mssql,
SQLite,
H2,
DM8,
YourDatabase,
Unknown;
companion object {
fun fromName(name: String) =
YourDatabase.takeIf { name.equals("Your Database", ignoreCase = true) }
?: Mssql.takeIf { name == "Microsoft SQL Server" }
?: Postgres.takeIf { name == "PostgreSQL" }
?: entries.first { it.name.uppercase() == name.uppercase() }
}
}result
JDBC metadata databaseProductName = "Your Database"
DBType.fromName("Your Database") == DBType.YourDatabase2. 配置 SQL 方言
目标数据库可以复用标准渲染规则时,直接配置 SqlDialect。
kotlin
val YourDatabaseDialect = SqlDialect(
leftQuote = """,
rightQuote = """,
standardEscapeStrings = true,
limitStyle = SqlLimitStyle.LimitOffset,
family = SqlDialectFamily.Standard
)用一个最小查询检查引用符和分页。
check
val sql = SqlQuery.Select(
from = listOf(SqlTable.Ident("user")),
limit = SqlLimit.limit(limit = 10, offset = 20)
).toSql(YourDatabaseDialect)sql
SELECT * FROM "user" LIMIT 10 OFFSET 203. 添加专属渲染器
目标数据库需要专属语法时,为它添加 SqlDialectFamily 和 renderer 分支。
family
enum class SqlDialectFamily {
Standard,
MySql,
PostgreSql,
SQLite,
H2,
Oracle,
SqlServer,
YourDatabase
}
val YourDatabaseDialect = SqlDialect(
leftQuote = """,
rightQuote = """,
limitStyle = SqlLimitStyle.LimitOffset,
family = SqlDialectFamily.YourDatabase
)renderer
class YourDatabaseSqlRenderer : StandardSqlRenderer(YourDatabaseDialect) {
override fun renderLimit(limit: SqlLimit): String = buildString {
limit.fetch?.let { append("LIMIT ${renderExpr(it.limit)}") }
limit.offset?.let { append(" OFFSET ${renderExpr(it)}") }
}
}
fun sqlRenderer(dialect: SqlDialect = SqlDialect.Standard): SqlRenderer = when (dialect.family) {
SqlDialectFamily.YourDatabase -> YourDatabaseSqlRenderer()
SqlDialectFamily.Standard -> StandardSqlRenderer(dialect)
SqlDialectFamily.MySql -> MysqlSqlRenderer(standardEscapeStrings = dialect.standardEscapeStrings)
SqlDialectFamily.PostgreSql -> PostgresqlSqlRenderer()
SqlDialectFamily.SQLite -> SqliteSqlRenderer()
SqlDialectFamily.H2 -> H2SqlRenderer()
SqlDialectFamily.Oracle -> OracleSqlRenderer()
SqlDialectFamily.SqlServer -> SqlServerSqlRenderer()
}check
val sql = SqlQuery.Select(
from = listOf(SqlTable.Ident("user")),
limit = SqlLimit.limit(limit = 5, offset = 10)
).toSql(YourDatabaseDialect)sql
SELECT * FROM "user" LIMIT 5 OFFSET 104. 读取表结构
DatabaseStatements 提供表存在性、字段、索引和注释读取语句。下面片段展示 metadata 查询和结果映射方式。
kotlin
override fun databaseName(wrapper: KronosDataSourceWrapper): String =
wrapper.url.substringBefore("?").substringAfterLast("/")
override fun tableExists(): SqlQuery = SqlQuery.Select(
select = listOf(SqlSelectItem.Expr(SqlExpr.UnsafeRaw("COUNT(*)"))),
from = listOf(
SqlTable.Ident(
"information_schema.tables",
identifier = SqlIdentifier.of("information_schema", "tables")
)
),
where = SqlExpr.UnsafeRaw("table_schema = :dbName AND table_name = :tableName")
)
override fun tableColumns(tableName: String): SqlQuery = SqlQuery.Select(
select = listOf(
SqlSelectItem.Expr(SqlExpr.UnsafeRaw("column_name AS COLUMN_NAME")),
SqlSelectItem.Expr(SqlExpr.UnsafeRaw("data_type AS DATA_TYPE")),
SqlSelectItem.Expr(SqlExpr.UnsafeRaw("character_maximum_length AS LENGTH")),
SqlSelectItem.Expr(SqlExpr.UnsafeRaw("is_nullable AS IS_NULLABLE"))
),
from = listOf(
SqlTable.Ident(
"information_schema.columns",
identifier = SqlIdentifier.of("information_schema", "columns")
)
),
where = SqlExpr.UnsafeRaw("table_schema = :dbName AND table_name = :tableName")
)
override fun mapColumns(tableName: String, rows: List<Map<String, Any>>): List<Field> =
rows.map {
Field(
columnName = it.cell("COLUMN_NAME").toString(),
type = KColumnType.fromString(it.cell("DATA_TYPE").toString()),
length = it.cell("LENGTH").asInt(),
tableName = tableName,
nullable = it.cell("IS_NULLABLE") == "YES"
)
}
override fun tableIndexes(tableName: String): SqlQuery = SqlQuery.Select(
select = listOf(
SqlSelectItem.Expr(SqlExpr.UnsafeRaw("index_name AS indexName")),
SqlSelectItem.Expr(SqlExpr.UnsafeRaw("column_name AS columnName"))
),
from = listOf(
SqlTable.Ident(
"information_schema.statistics",
identifier = SqlIdentifier.of("information_schema", "statistics")
)
),
where = SqlExpr.UnsafeRaw("table_schema = :dbName AND table_name = :tableName")
)
override fun mapIndexes(tableName: String, rows: List<Map<String, Any>>): List<KTableIndex> =
rows.groupBy { it.cell("indexName").toString() }.map { (name, columns) ->
KTableIndex(
name = name,
columns = columns.map { it.cell("columnName").toString() }.toTypedArray()
)
}sql
SELECT COUNT(*)
FROM "information_schema"."tables"
WHERE table_schema = :dbName AND table_name = :tableName5. 生成 DDL 语句
同一个 YourDatabaseStatements 继续负责建表、删表、清表和表结构同步。
kotlin
override fun createTable(input: DatabaseCreateTable): List<SqlStatement> {
val table = SqlIdentifier.of(input.tableName)
return listOf(
SqlDdlStatement.CreateTable(
tableName = table,
columns = input.columns.map { it.toColumnDefinition(::columnType) },
comment = input.tableComment,
ifNotExists = true
)
) + input.indexes.map { it.toCreateIndexStatement(table) }
}
override fun dropTable(tableName: String, ifExists: Boolean): List<SqlStatement> =
listOf(SqlDdlStatement.DropTable(SqlIdentifier.of(tableName), ifExists))
override fun truncateTable(tableName: String, restartIdentity: Boolean): List<SqlStatement> =
listOf(SqlDmlStatement.Truncate(SqlTable.Ident(tableName), restartIdentity))
override fun syncTable(input: DatabaseSyncTable): List<SqlStatement> {
val table = SqlIdentifier.of(input.tableName)
return buildList {
addAll(input.columns.toAdd.map { (column, _) ->
SqlDdlStatement.AlterTable.AddColumn(table, column.toColumnDefinition(::columnType))
})
addAll(input.columns.toModified.map { (column, _, _) ->
SqlDdlStatement.AlterTable.ModifyColumn(table, column.toColumnDefinition(::columnType))
})
addAll(input.columns.toDelete.map {
SqlDdlStatement.AlterTable.DropColumn(table, SqlIdentifier.of(it.columnName))
})
addAll(input.indexes.toDelete.map { SqlDdlStatement.DropIndex(SqlIdentifier.of(it.name), table) })
addAll(input.indexes.toAdd.map { it.toCreateIndexStatement(table) })
}
}
private fun columnType(type: KColumnType, length: Int, scale: Int): String = when (type) {
KColumnType.INT -> "INTEGER"
KColumnType.VARCHAR -> "VARCHAR(${length.takeIf { it > 0 } ?: 255})"
KColumnType.DATETIME -> "TIMESTAMP"
else -> "VARCHAR(255)"
}sql
CREATE TABLE IF NOT EXISTS "account" (
"id" INTEGER NOT NULL PRIMARY KEY,
"name" VARCHAR(64)
)6. 注册方言
SqlManager.registerDatabase(...) 同时注册 SQL 方言和表结构语句。
kotlin
SqlManager.registerDatabase(
DBType.YourDatabase,
YourDatabaseDialect,
YourDatabaseStatements
)check
check(SqlManager.dialectOf(DBType.YourDatabase) == YourDatabaseDialect)
check(SqlManager.statementsOf(DBType.YourDatabase) == YourDatabaseStatements)7. 连接数据库
JDBC metadata 能映射到 DBType.YourDatabase 后,KronosJdbcWrapper 会带出对应方言。
kotlin
val wrapper = KronosJdbcWrapper(dataSource)
check(wrapper.dbType == DBType.YourDatabase)
check(wrapper.sqlDialect == YourDatabaseDialect)
Kronos.dataSource = { wrapper }result
wrapper.dbType == DBType.YourDatabase
wrapper.sqlDialect == YourDatabaseDialect8. 验证查询
先验证最小 select,确认引用符、表名和字段名正确。
kotlin
val (sql, _) = User()
.select { [it.id, it.name] }
.build(wrapper)
check(sql == """SELECT "id", "name" FROM "user"""")9. 验证分页
分页验证覆盖 limitStyle 和 renderer 的 renderLimit(...)。
kotlin
val (_, pageTask) = User()
.select()
.orderBy { it.id.asc() }
.page(2, 20)
.withTotal()
.build(wrapper)
val (sql, _) = pageTask
check(sql.contains("LIMIT 20 OFFSET 20"))10. 验证 Upsert
upsert 验证覆盖冲突字段和更新字段渲染。
kotlin
val (sql, _) = User(id = 1, name = "Ada")
.upsert()
.on { it.id }
.set { [it.name] }
.build(wrapper)
check(sql.contains("ON CONFLICT"))11. 验证表结构同步
表结构同步验证覆盖 metadata 查询、字段差异、索引差异和 DDL 执行。
kotlin
val created = wrapper.table.syncTable(User())
if (created) {
wrapper.table.truncateTable(User())
}result
created == false // 表不存在,已创建表
created == true // 表已存在,已执行同步12. 验证函数
函数验证覆盖普通函数名、数据库专属函数名和表达式参数。
kotlin
val (sql, _) = User()
.select { f.rand().alias("rand") }
.build(wrapper)
check(sql.contains("RAND()") || sql.contains("RANDOM()"))13. 更新文档入口
方言通过构建和集成验证后,把数据库名称加入📚数据库方言支持页面。
| Your Database | `DBType.YourDatabase` |