From d74687ec508549f3521b4836c5e4635f43d1f286 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Mon, 20 Jun 2022 15:59:00 +0200 Subject: [PATCH] Update README file ; add --quoted and --uppercase options --- README.md | 10 ++- build.gradle.kts | 2 +- .../kotlin/com/republicate/kddl/Main.kt | 7 +- .../com/republicate/kddl/SQLFormatter.kt | 75 ++++++++++-------- .../kotlin/com/republicate/kddl/format.kt | 2 +- .../kddl/hypersql/HyperSQLFormatter.kt | 11 ++- .../republicate/kddl/postgresql/PostgreSQL.kt | 79 ++++++++++--------- 7 files changed, 106 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index f9e6d9c..a55ea79 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,12 @@ kddl [OPTIONS] > [output_file] Options: ``` - --input, -i -> mandatory; input file or JDBC URL (with credentials) - --format, -f -> mandatory; output format { Value should be one of [kddl, plantuml, postgresql] } - --driver, -d -> optional; jdbc driver, needed when input is a JDBC URL { String } - --help, -h -> Usage info + -i, --input -> mandatory; input file or JDBC URL (with credentials) + -f, --format -> mandatory; output format (value should be one of [kddl, plantuml, postgresql]) + -d, --driver -> jdbc driver, needed when input is a JDBC URL (classname, must be present in the classpath) + -q, --quoted -> quoted identifiers + -u, --uppercase -> uppercase identifiers + -h, --help -> Usage info ``` diff --git a/build.gradle.kts b/build.gradle.kts index d7ca688..0528357 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ plugins { } group = "com.republicate.kddl" -version = "0.6.1" +version = "0.7" repositories { mavenCentral() diff --git a/src/commonMain/kotlin/com/republicate/kddl/Main.kt b/src/commonMain/kotlin/com/republicate/kddl/Main.kt index c1a71b8..fcc777d 100644 --- a/src/commonMain/kotlin/com/republicate/kddl/Main.kt +++ b/src/commonMain/kotlin/com/republicate/kddl/Main.kt @@ -6,6 +6,7 @@ import com.republicate.kddl.plantuml.PlantUMLFormatter import com.republicate.kddl.postgresql.PostgreSQLFormatter import kotlinx.cli.ArgParser import kotlinx.cli.ArgType +import kotlinx.cli.default import kotlinx.cli.required val argParser = ArgParser("kddl") @@ -21,6 +22,8 @@ fun main(args: Array) { val input by argParser.option(ArgType.String, shortName = "i", description = "input file or url").required() val format by argParser.option(ArgType.Choice(), shortName = "f", description = "output format").required() val driver by argParser.option(ArgType.String, shortName = "d", description = "jdbc driver") + val uppercase by argParser.option(ArgType.Boolean, shortName = "u", description = "uppercase identifiers").default(false) + val quoted by argParser.option(ArgType.Boolean, shortName = "q", description = "quoted identifiers").default(false) argParser.parse(args) val tree = when { @@ -36,8 +39,8 @@ fun main(args: Array) { val formatter = when (format) { Format.KDDL -> KDDLFormatter() Format.PLANTUML -> PlantUMLFormatter() - Format.POSTGRESQL -> PostgreSQLFormatter() - Format.HYPERSQL -> HyperSQLFormatter() + Format.POSTGRESQL -> PostgreSQLFormatter(quoted=quoted, uppercase=uppercase) + Format.HYPERSQL -> HyperSQLFormatter(quoted=quoted, uppercase=uppercase) else -> throw IllegalArgumentException("invalid format") } val ret = formatter.format(tree) diff --git a/src/commonMain/kotlin/com/republicate/kddl/SQLFormatter.kt b/src/commonMain/kotlin/com/republicate/kddl/SQLFormatter.kt index 0ceb73c..c24769d 100644 --- a/src/commonMain/kotlin/com/republicate/kddl/SQLFormatter.kt +++ b/src/commonMain/kotlin/com/republicate/kddl/SQLFormatter.kt @@ -1,16 +1,24 @@ package com.republicate.kddl -abstract class SQLFormatter: Formatter { +import com.republicate.kddl.Formatter.Companion.EOL + +abstract class SQLFormatter(val quoted: Boolean, val uppercase: Boolean): Formatter { open val supportsEnums = false open val supportsInheritance = false + open val scopedObjectNames = false open fun defineEnum(field: ASTField) = "" open fun defineInheritedView(table: ASTTable) = "" open fun setSchema(schema: String) = "SET SCHEMA $schema$END" - val END = ";${Formatter.EOL}" - val Q = "\"" + val END = ";${EOL}" + val Q = if (quoted) "\"" else "" val upper = Regex("[A-Z]") + val transform: (String)->String = if (uppercase) { + { camelToSnake(it).uppercase() } + } else { + { camelToSnake(it) } + } private val typeMap = mapOf( "datetime" to "timestamp", @@ -23,7 +31,7 @@ abstract class SQLFormatter: Formatter { open fun mapType(type: String) = typeMap[type] // CB TODO - make it configurable - fun camelToSnake(camel : String) : String { + private fun camelToSnake(camel : String) : String { val ret = StringBuilder() var pos = 0 upper.findAll(camel).forEach { @@ -38,20 +46,20 @@ abstract class SQLFormatter: Formatter { } override fun format(asm: ASTDatabase, indent: String): String { - val ret = StringBuilder("-- database ${asm.name}${Formatter.EOL}") + val ret = StringBuilder("-- database ${asm.name}${EOL}") // TODO postgresql options ret.append( asm.schemas.map { format(it.value, indent) - }.joinToString(separator = Formatter.EOL) + }.joinToString(separator = EOL) ) return ret.toString() } override fun format(asm: ASTSchema, indent: String): String { val ret = StringBuilder() - val schemaName = camelToSnake(asm.name) - ret.append("${Formatter.EOL}-- schema $schemaName${Formatter.EOL}") + val schemaName = transform(asm.name) + ret.append("${EOL}-- schema $schemaName${EOL}") ret.append("DROP SCHEMA IF EXISTS $schemaName CASCADE$END") ret.append("CREATE SCHEMA $schemaName") // incorrect @@ -67,13 +75,13 @@ abstract class SQLFormatter: Formatter { it.type.startsWith("enum(") }.map { defineEnum(it) - }.joinToString(separator = Formatter.EOL)) + }.joinToString(separator = EOL)) } - ret.append(Formatter.EOL) + ret.append(EOL) ret.append( asm.tables.values.map { format(it, indent) - }.joinToString(separator = Formatter.EOL) + }.joinToString(separator = EOL) ) asm.tables.values.flatMap{ it.foreignKeys }/*.filter { !it.isFieldLink() @@ -92,7 +100,7 @@ abstract class SQLFormatter: Formatter { field -> ASTField(tbl, field.name, field.type) }.toSet() val fk = ASTForeignKey(tbl, fkFields, parent, true, true, true) - ret.append(format(fk, indent)).append(Formatter.EOL) + ret.append(format(fk, indent)).append(EOL) } } return ret.toString() @@ -100,7 +108,7 @@ abstract class SQLFormatter: Formatter { override fun format(asm: ASTTable, indent: String): String { val ret = StringBuilder() - var tableName = camelToSnake(asm.name) + var tableName = transform(asm.name) var viewName : String? = null; if (asm.parent != null) { @@ -113,7 +121,7 @@ abstract class SQLFormatter: Formatter { for (field in asm.fields.values.filter { it.primaryKey }) { if (firstField) firstField = false else ret.append(",") - ret.append(Formatter.EOL) + ret.append(EOL) ret.append(format(field, " ")) } @@ -121,14 +129,14 @@ abstract class SQLFormatter: Formatter { if (asm.parent != null) { for (field in asm.parent.getPrimaryKey()) { if (firstField) firstField = false else ret.append(",") - ret.append(Formatter.EOL) + ret.append(EOL) ret.append(format(field, " ")) } } for (field in asm.fields.values.filter { !it.primaryKey }) { if (firstField) firstField = false else ret.append(",") - ret.append(Formatter.EOL) + ret.append(EOL) ret.append(format(field, " ")) } @@ -136,27 +144,27 @@ abstract class SQLFormatter: Formatter { if (asm.children.isNotEmpty()) { if (!supportsInheritance) throw Error("inheritance not supported") if (firstField) firstField = false else ret.append(",") - ret.append(Formatter.EOL) + ret.append(EOL) ret.append(" class varchar(30)") } if (asm.parent == null) { - val pkFields = asm.getPrimaryKey().map { camelToSnake(it.name) }.joinToString(",") + val pkFields = asm.getPrimaryKey().map { transform(it.name) }.joinToString(",") if (pkFields.isNotEmpty()) { if (firstField) firstField = false else ret.append(",") - ret.append(Formatter.EOL) + ret.append(EOL) ret.append(" PRIMARY KEY ($pkFields)") } } else { - val pkFields = asm.parent.getPrimaryKey().map { camelToSnake(it.name) }.joinToString(",") + val pkFields = asm.parent.getPrimaryKey().map { transform(it.name) }.joinToString(",") if (pkFields.isNotEmpty()) { if (firstField) firstField = false else ret.append(",") - ret.append(Formatter.EOL) + ret.append(EOL) ret.append(" PRIMARY KEY ($pkFields)") } } - ret.append("${Formatter.EOL})$END${Formatter.EOL}") + ret.append("${EOL})$END${EOL}") if (asm.parent != null) { ret.append(defineInheritedView(asm)) @@ -168,9 +176,9 @@ abstract class SQLFormatter: Formatter { override fun format(asm: ASTField, indent: String): String { val ret = StringBuilder(indent) asm.apply { - ret.append(camelToSnake(name)) + ret.append(transform(name)) if (type.isEmpty()) throw RuntimeException("Missing type for ${asm.table.schema.name}.${asm.table.name}.${asm.name}") - else if (type.startsWith("enum(")) ret.append(" enum_${camelToSnake(name)}") + else if (type.startsWith("enum(")) ret.append(" enum_${transform(name)}") else ret.append(" ${mapType(type) ?: type}") if (nonNull) ret.append(" NOT NULL") // CB TODO - review 'unique' upstream calculation. A field should not be systematically @@ -197,20 +205,23 @@ abstract class SQLFormatter: Formatter { val src = asm.from val ret = StringBuilder() val srcName = - if (src.parent == null) "$Q${camelToSnake(src.name)}$Q" - else "${Q}base_${camelToSnake(src.name)}$Q" + if (src.parent == null) "$Q${transform(src.name)}$Q" + else "${Q}base_${transform(src.name)}$Q" + val fkName = + if (scopedObjectNames) "$Q${transform(asm.fields.first().name.removeSuffix(suffix))}$Q" + else "$Q${transform(src.name)}_${transform(asm.fields.first().name.removeSuffix(suffix))}_fk$Q" ret.append("ALTER TABLE $srcName") - ret.append(" ADD CONSTRAINT $Q${camelToSnake(asm.fields.first().name.removeSuffix(suffix))}$Q") - ret.append(" FOREIGN KEY (${camelToSnake(asm.fields.map{it.name}.joinToString(","))})") + ret.append(" ADD CONSTRAINT $fkName") + ret.append(" FOREIGN KEY (${transform(asm.fields.map{it.name}.joinToString(","))})") ret.append(" REFERENCES ") if (asm.towards.schema.name != src.schema.name) { - ret.append("$Q${camelToSnake(asm.towards.schema.name)}$Q.") + ret.append("$Q${transform(asm.towards.schema.name)}$Q.") } val dstName = - if (asm.towards.parent == null) "$Q${camelToSnake(asm.towards.name)}$Q" - else "${Q}base_${camelToSnake(asm.towards.name)}$Q" - ret.append("$dstName (${camelToSnake(asm.towards.getOrCreatePrimaryKey().map{it.name}.joinToString(","))})") + if (asm.towards.parent == null) "$Q${transform(asm.towards.name)}$Q" + else "${Q}base_${transform(asm.towards.name)}$Q" + ret.append("$dstName (${transform(asm.towards.getOrCreatePrimaryKey().map{it.name}.joinToString(","))})") if (asm.cascade) { ret.append(" ON DELETE CASCADE") } diff --git a/src/commonMain/kotlin/com/republicate/kddl/format.kt b/src/commonMain/kotlin/com/republicate/kddl/format.kt index 0feabe9..36317e5 100644 --- a/src/commonMain/kotlin/com/republicate/kddl/format.kt +++ b/src/commonMain/kotlin/com/republicate/kddl/format.kt @@ -4,7 +4,7 @@ class SemanticException(message: String? = null, cause: Throwable? = null) : Exc interface Formatter { companion object { - val EOL: String = "\n" + const val EOL: String = "\n" } fun format(asm: ASTDatabase, indent: String = ""): String fun format(asm: ASTSchema, indent: String): String diff --git a/src/commonMain/kotlin/com/republicate/kddl/hypersql/HyperSQLFormatter.kt b/src/commonMain/kotlin/com/republicate/kddl/hypersql/HyperSQLFormatter.kt index 9ff1a43..ee38382 100644 --- a/src/commonMain/kotlin/com/republicate/kddl/hypersql/HyperSQLFormatter.kt +++ b/src/commonMain/kotlin/com/republicate/kddl/hypersql/HyperSQLFormatter.kt @@ -2,5 +2,14 @@ package com.republicate.kddl.hypersql import com.republicate.kddl.SQLFormatter -class HyperSQLFormatter: SQLFormatter() { +class HyperSQLFormatter(quoted: Boolean, uppercase: Boolean): SQLFormatter(quoted, uppercase) { + + override fun mapType(type: String): String? { + return when (type) { + "serial" -> "integer generated by default as identity" + // "bigserial" TODO + else -> super.mapType(type) + } + } + } diff --git a/src/commonMain/kotlin/com/republicate/kddl/postgresql/PostgreSQL.kt b/src/commonMain/kotlin/com/republicate/kddl/postgresql/PostgreSQL.kt index 3614b32..ccf9b26 100644 --- a/src/commonMain/kotlin/com/republicate/kddl/postgresql/PostgreSQL.kt +++ b/src/commonMain/kotlin/com/republicate/kddl/postgresql/PostgreSQL.kt @@ -3,34 +3,35 @@ package com.republicate.kddl.postgresql import com.republicate.kddl.* import com.republicate.kddl.Formatter.Companion.EOL -class PostgreSQLFormatter: SQLFormatter() { +class PostgreSQLFormatter(quoted: Boolean, uppercase: Boolean): SQLFormatter(quoted, uppercase) { override val supportsEnums = true override val supportsInheritance = true + override val scopedObjectNames = true override fun defineEnum(field: ASTField) = - "CREATE TYPE enum_${camelToSnake(field.name)} AS ENUM ${field.type.substring(4)};${Formatter.EOL}" + - "CREATE CAST (varchar AS enum_${camelToSnake(field.name)}) WITH INOUT AS IMPLICIT;" + "CREATE TYPE ${transform("Enum${field.name}")} AS ENUM ${field.type.substring(4)};${EOL}" + + "CREATE CAST (varchar AS enum_${transform(field.name)}) WITH INOUT AS IMPLICIT;" override fun defineInheritedView(table: ASTTable): String { val ret = StringBuilder() val parent = table.parent!! - val viewName = camelToSnake(table.name) + val viewName = transform(table.name) val tableName = "base_${viewName}" // View - val parentName = "$Q${camelToSnake(table.parent.name)}$Q" + val parentName = "$Q${transform(table.parent.name)}$Q" val qualifiedParentName = if (table.schema == table.parent.schema) parentName else "$Q${parent.schema.name}$Q.$parentName" - ret.append("CREATE VIEW $viewName AS${Formatter.EOL} SELECT${Formatter.EOL} ") + ret.append("CREATE VIEW $viewName AS${EOL} SELECT${EOL} ") val parentPkFields = parent.getPrimaryKey().map { - "$parentName.${camelToSnake(it.name)}" + "$parentName.${transform(it.name)}" }.joinToString(",") ret.append(parentPkFields) val parentNonPKFields = parent.fields.values .filter { !it.primaryKey } - .map { camelToSnake(it.name) } + .map { transform(it.name) } .joinToString(",") if (parentNonPKFields.isNotEmpty()) { ret.append(",") @@ -38,45 +39,45 @@ class PostgreSQLFormatter: SQLFormatter() { } ret.append(",class") val childFields = table.fields.values - .map { camelToSnake(it.name) } + .map { transform(it.name) } .joinToString(",") if (childFields.isNotEmpty()) { - ret.append(",${Formatter.EOL}") + ret.append(",${EOL}") ret.append(" $childFields") } - ret.append(Formatter.EOL) + ret.append(EOL) ret.append(" FROM $Q$tableName$Q JOIN $qualifiedParentName ON ") val join = parent.getPrimaryKey().map { "$parentName.${it.name} = $Q$tableName$Q.${it.name}" }.joinToString(" AND ") ret.append(join) - ret.append("$END${Formatter.EOL}") + ret.append("$END${EOL}") // Rules (only for single field primary key for now) if (parent.getPrimaryKey().size == 1) { val pk = parent.getPrimaryKey().elementAt(0) - val pkName = camelToSnake(pk.name) + val pkName = transform(pk.name) if (pk.type == "serial") { - var seqName = "${camelToSnake(parent.name)}_${pkName}_seq" + var seqName = "${transform(parent.name)}_${pkName}_seq" if (table.schema != parent.schema) seqName = "$Q${parent.schema.name}$Q.$seqName" - ret.append("CREATE RULE ${Q}insert_${viewName}$Q AS ON INSERT TO $Q$viewName$Q DO INSTEAD (${Formatter.EOL}") + ret.append("CREATE RULE ${Q}insert_${viewName}$Q AS ON INSERT TO $Q$viewName$Q DO INSTEAD (${EOL}") - ret.append(" INSERT INTO $qualifiedParentName ($pkName, $parentNonPKFields,class)${Formatter.EOL} VALUES (") + ret.append(" INSERT INTO $qualifiedParentName ($pkName, $parentNonPKFields,class)${EOL} VALUES (") ret.append(" COALESCE(NEW.$pkName,NEXTVAL('$seqName')),") - var parentValues = parent.fields.values.filter { !it.primaryKey }.map { "NEW.${camelToSnake(it.name)}" }.joinToString(",") - ret.append("$parentValues,'$viewName')${Formatter.EOL}") + var parentValues = parent.fields.values.filter { !it.primaryKey }.map { "NEW.${transform(it.name)}" }.joinToString(",") + ret.append("$parentValues,'$viewName')${EOL}") ret.append(" RETURNING $qualifiedParentName.*") if (childFields.isNotEmpty()) { table.fields.values.forEach { var nullType = when { // CB TODO - redundant with types map below it.type.startsWith("varchar") -> "null::varchar" - it.type.startsWith("enum") -> "null::enum_${camelToSnake(it.name)}" + it.type.startsWith("enum") -> "null::enum_${transform(it.name)}" it.type == "float" -> "null::real" it.type == "double" -> "null::float" it.type == "int" -> "null::integer" @@ -85,7 +86,7 @@ class PostgreSQLFormatter: SQLFormatter() { ret.append(",$nullType") } } - ret.append("$END${Formatter.EOL}") + ret.append("$END${EOL}") ret.append(" SELECT SETVAL('$seqName', (SELECT MAX($pkName) FROM $qualifiedParentName)) $pkName$END") @@ -93,58 +94,58 @@ class PostgreSQLFormatter: SQLFormatter() { if (childFields.isNotEmpty()) { ret.append(",$childFields") } - ret.append(")${Formatter.EOL} VALUES (") + ret.append(")${EOL} VALUES (") ret.append("CURRVAL('$seqName')") - var childValues = table.fields.values.map { "NEW.${camelToSnake(it.name)}" }.joinToString(",") + var childValues = table.fields.values.map { "NEW.${transform(it.name)}" }.joinToString(",") if (childValues.isNotEmpty()) { ret.append(",$childValues") } ret.append(")$END") - ret.append(")$END${Formatter.EOL}") + ret.append(")$END${EOL}") } else { - ret.append("CREATE RULE ${Q}insert_${viewName}$Q AS ON INSERT TO $Q$viewName$Q DO INSTEAD (${Formatter.EOL}") - ret.append(" INSERT INTO $qualifiedParentName ($pkName,$parentNonPKFields,class)${Formatter.EOL} VALUES (") - val parentValues = parent.fields.values.map { "NEW.${camelToSnake(it.name)}" }.joinToString(",") + ret.append("CREATE RULE ${Q}insert_${viewName}$Q AS ON INSERT TO $Q$viewName$Q DO INSTEAD (${EOL}") + ret.append(" INSERT INTO $qualifiedParentName ($pkName,$parentNonPKFields,class)${EOL} VALUES (") + val parentValues = parent.fields.values.map { "NEW.${transform(it.name)}" }.joinToString(",") ret.append("$parentValues,'$viewName')$END") ret.append(" INSERT INTO $Q$tableName$Q ($pkName") if (childFields.isNotEmpty()) { ret.append(",$childFields") } - ret.append(")${Formatter.EOL} VALUES (") - var childValues = table.fields.values.map { "NEW.${camelToSnake(it.name)}" }.joinToString(",") + ret.append(")${EOL} VALUES (") + var childValues = table.fields.values.map { "NEW.${transform(it.name)}" }.joinToString(",") ret.append("NEW.$pkName") if (childValues.isNotEmpty()) { ret.append(",$childValues") } ret.append(")$END"); - ret.append(")$END${Formatter.EOL}") + ret.append(")$END${EOL}") } - ret.append("CREATE RULE ${Q}update_${viewName}$Q AS ON UPDATE TO $Q$viewName$Q DO INSTEAD (${Formatter.EOL}") - ret.append(" UPDATE $qualifiedParentName${Formatter.EOL}") + ret.append("CREATE RULE ${Q}update_${viewName}$Q AS ON UPDATE TO $Q$viewName$Q DO INSTEAD (${EOL}") + ret.append(" UPDATE $qualifiedParentName${EOL}") ret.append(" SET ") val updateParent = parent.fields.values.filter { !it.primaryKey } - .map { "${camelToSnake(it.name)} = NEW.${camelToSnake(it.name)}" } + .map { "${transform(it.name)} = NEW.${transform(it.name)}" } .joinToString(",") - ret.append("$updateParent${Formatter.EOL} WHERE $pkName = NEW.$pkName${Formatter.EOL}") + ret.append("$updateParent${EOL} WHERE $pkName = NEW.$pkName${EOL}") ret.append(" RETURNING NEW.*$END") val updateChild = table.fields.values - .map { "${camelToSnake(it.name)} = NEW.${camelToSnake(it.name)}" } + .map { "${transform(it.name)} = NEW.${transform(it.name)}" } .joinToString(",") if (updateChild.isNotEmpty()) { - ret.append(" UPDATE $Q$tableName$Q${Formatter.EOL}") + ret.append(" UPDATE $Q$tableName$Q${EOL}") ret.append(" SET ") - ret.append("$updateChild${Formatter.EOL} WHERE $pkName = NEW.$pkName$END") + ret.append("$updateChild${EOL} WHERE $pkName = NEW.$pkName$END") } - ret.append(")$END${Formatter.EOL}") + ret.append(")$END${EOL}") - ret.append("CREATE RULE ${Q}delete_${viewName}$Q AS ON DELETE TO $Q$viewName$Q DO INSTEAD (${Formatter.EOL}") + ret.append("CREATE RULE ${Q}delete_${viewName}$Q AS ON DELETE TO $Q$viewName$Q DO INSTEAD (${EOL}") // rely on cascade ret.append(" DELETE FROM $qualifiedParentName WHERE $pkName = OLD.$pkName$END") - ret.append(")$END${Formatter.EOL}") + ret.append(")$END${EOL}") } else { throw Error("inheritance only supported for single field primary key") }