diff --git a/README.md b/README.md index 8639ac37741a..08d16048effc 100644 --- a/README.md +++ b/README.md @@ -1139,6 +1139,7 @@ Here is a list of template creators: * GraphQL: @wing328 [:heart:](https://www.patreon.com/wing328) * Ktorm: @Luiz-Monad * MySQL: [@ybelenko](https://github.com/ybelenko) + * PostgreSQL: [@iri](https://github.com/iri) * Postman Collection: @gcatanese * Protocol Buffer: @wing328 * WSDL: @adessoDpd diff --git a/bin/configs/postgresql-schema-petstore.yaml b/bin/configs/postgresql-schema-petstore.yaml new file mode 100644 index 000000000000..2ab2773849ba --- /dev/null +++ b/bin/configs/postgresql-schema-petstore.yaml @@ -0,0 +1,5 @@ +generatorName: postgresql-schema +outputDir: samples/schema/petstore/postgresql +inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml +templateDir: modules/openapi-generator/src/main/resources/postgresql-schema + diff --git a/bin/utils/openapi-generator-cli.sh b/bin/utils/openapi-generator-cli.sh old mode 100644 new mode 100755 diff --git a/docs/generators.md b/docs/generators.md index f3ed0d410525..2d29254e2d49 100644 --- a/docs/generators.md +++ b/docs/generators.md @@ -168,6 +168,7 @@ The following generators are available: * [graphql-schema](generators/graphql-schema.md) * [ktorm-schema (beta)](generators/ktorm-schema.md) * [mysql-schema](generators/mysql-schema.md) +* [postgresql-schema (beta)](generators/postgresql-schema.md) * [postman-collection (beta)](generators/postman-collection.md) * [protobuf-schema (beta)](generators/protobuf-schema.md) * [wsdl-schema (beta)](generators/wsdl-schema.md) diff --git a/docs/generators/postgresql-schema.md b/docs/generators/postgresql-schema.md new file mode 100644 index 000000000000..b4959234ae6e --- /dev/null +++ b/docs/generators/postgresql-schema.md @@ -0,0 +1,1031 @@ +--- +title: Documentation for the postgresql-schema Generator +--- + +## METADATA + +| Property | Value | Notes | +| -------- | ----- | ----- | +| generator name | postgresql-schema | pass this to the generate command after -g | +| generator stability | BETA | | +| generator type | SCHEMA | | +| generator language | Postgresql | | +| generator default templating engine | mustache | | +| helpTxt | Generates a PostgreSQL schema based on the schema defined in the OpenAPI specification (v2, v3) | | + +## CONFIG OPTIONS +These options may be applied as additional-properties (cli) or configOptions (plugins). Refer to [configuration docs](https://openapi-generator.tech/docs/configuration) for more details. + +| Option | Description | Values | Default | +| ------ | ----------- | ------ | ------- | +|defaultDatabaseName|Database name that will be used for all generated PostgreSQL DDL and DML statements.| || +|idAutoIncEnabled|If `true`, generates autoincrement PostgreSQL types `SERIAL` and `BIGSERIAL` for `int32` and `int64` respectively for integer fields with name 'id'.| |false| +|identifierNamingConvention|Naming convention of PostgreSQL idebntifiers (table names and column names).|
**snake_case**
Transform named to 'snake_case'.
**original**
Leave original names as in `YAML` file.
|snake_case| +|jsonDataType|Use of PostgreSQL data types for complex model properties.|
**json**
Generate `JSON` fields. Value is stored in `JSON` data type field as human-readable text. Value compliance with JSON standard is checked.
**jsonb**
Generate `JSONB` fields. Value is stored in `JSONB` data type field in binary format. `JSONB` data type is generally nore efficient than `JSON` but it is not human-readable. Value compliance with JSON standard is checked.
**off**
Generate `TEXT` fields. Just store the value as plain text. Value compliance with JSON standard is not checked.
|json| +|namedParametersEnabled|Generates query examples with named variables in value placeholders (eg.`:name`,`:quantity`) if `true`. Otherwise, generates question marks `?` in value placeholders.| |false| + +## IMPORT MAPPING + +| Type/Alias | Imports | +| ---------- | ------- | + + +## INSTANTIATION TYPES + +| Type/Alias | Instantiated By | +| ---------- | --------------- | + + +## LANGUAGE PRIMITIVES + + + +## RESERVED WORDS + + + +## FEATURE SET + + +### Client Modification Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|BasePath|✗|ToolingExtension +|Authorizations|✗|ToolingExtension +|UserAgent|✗|ToolingExtension +|MockServer|✗|ToolingExtension + +### Data Type Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Custom|✗|OAS2,OAS3 +|Int32|✓|OAS2,OAS3 +|Int64|✓|OAS2,OAS3 +|Float|✓|OAS2,OAS3 +|Double|✓|OAS2,OAS3 +|Decimal|✓|ToolingExtension +|String|✓|OAS2,OAS3 +|Byte|✓|OAS2,OAS3 +|Binary|✓|OAS2,OAS3 +|Boolean|✓|OAS2,OAS3 +|Date|✓|OAS2,OAS3 +|DateTime|✓|OAS2,OAS3 +|Password|✓|OAS2,OAS3 +|File|✓|OAS2 +|Uuid|✗| +|Array|✓|OAS2,OAS3 +|Null|✗|OAS3 +|AnyType|✗|OAS2,OAS3 +|Object|✓|OAS2,OAS3 +|Maps|✓|ToolingExtension +|CollectionFormat|✓|OAS2 +|CollectionFormatMulti|✓|OAS2 +|Enum|✓|OAS2,OAS3 +|ArrayOfEnum|✓|ToolingExtension +|ArrayOfModel|✓|ToolingExtension +|ArrayOfCollectionOfPrimitives|✓|ToolingExtension +|ArrayOfCollectionOfModel|✓|ToolingExtension +|ArrayOfCollectionOfEnum|✓|ToolingExtension +|MapOfEnum|✓|ToolingExtension +|MapOfModel|✓|ToolingExtension +|MapOfCollectionOfPrimitives|✓|ToolingExtension +|MapOfCollectionOfModel|✓|ToolingExtension +|MapOfCollectionOfEnum|✓|ToolingExtension + +### Documentation Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Readme|✓|ToolingExtension +|Model|✓|ToolingExtension +|Api|✓|ToolingExtension + +### Global Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Host|✓|OAS2,OAS3 +|BasePath|✓|OAS2,OAS3 +|Info|✓|OAS2,OAS3 +|Schemes|✗|OAS2,OAS3 +|PartialSchemes|✓|OAS2,OAS3 +|Consumes|✓|OAS2 +|Produces|✓|OAS2 +|ExternalDocumentation|✓|OAS2,OAS3 +|Examples|✓|OAS2,OAS3 +|XMLStructureDefinitions|✗|OAS2,OAS3 +|MultiServer|✗|OAS3 +|ParameterizedServer|✗|OAS3 +|ParameterStyling|✗|OAS3 +|Callbacks|✗|OAS3 +|LinkObjects|✗|OAS3 + +### Parameter Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Path|✓|OAS2,OAS3 +|Query|✓|OAS2,OAS3 +|Header|✓|OAS2,OAS3 +|Body|✓|OAS2 +|FormUnencoded|✓|OAS2 +|FormMultipart|✓|OAS2 +|Cookie|✓|OAS3 + +### Schema Support Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Simple|✓|OAS2,OAS3 +|Composite|✓|OAS2,OAS3 +|Polymorphism|✗|OAS2,OAS3 +|Union|✗|OAS3 +|allOf|✗|OAS2,OAS3 +|anyOf|✗|OAS3 +|oneOf|✗|OAS3 +|not|✗|OAS3 + +### Security Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|BasicAuth|✗|OAS2,OAS3 +|ApiKey|✗|OAS2,OAS3 +|OpenIDConnect|✗|OAS3 +|BearerToken|✗|OAS3 +|OAuth2_Implicit|✗|OAS2,OAS3 +|OAuth2_Password|✗|OAS2,OAS3 +|OAuth2_ClientCredentials|✗|OAS2,OAS3 +|OAuth2_AuthorizationCode|✗|OAS2,OAS3 +|SignatureAuth|✗|OAS3 +|AWSV4Signature|✗|ToolingExtension + +### Wire Format Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|JSON|✗|OAS2,OAS3 +|XML|✗|OAS2,OAS3 +|PROTOBUF|✗|ToolingExtension +|Custom|✗|OAS2,OAS3 diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/GeneratorLanguage.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/GeneratorLanguage.java index a75901cd54e9..bbbd8fe1b725 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/GeneratorLanguage.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/GeneratorLanguage.java @@ -30,7 +30,7 @@ public enum GeneratorLanguage { JAVASCRIPT("Javascript"), GRAPH_QL("GraphQL"), GROOVY("Groovy"), HASKELL("Haskell"), HTTP("Jetbrains HTTP Client (HTTP/REST)"), TYPESCRIPT("Typescript"), K_SIX("k6"), KOTLIN("Kotlin"), KTORM("Ktorm"), LUA("Lua"), MYSQL("Mysql"), NIM("Nim"), - OBJECTIVE_C("Objective-C"), OCAML("OCaml"), PERL("Perl"), PHP("PHP"), + OBJECTIVE_C("Objective-C"), OCAML("OCaml"), PERL("Perl"), PHP("PHP"), POSTGRESQL("Postgresql"), POWERSHELL("PowerShell"), PROTOBUF("Protocol Buffers (Protobuf)"), PYTHON("Python"), R("R"), RUBY("Ruby"), RUST("Rust"), SCALA("Scala"), SWIFT("Swift"), WSDL("Web Services Description Language (WSDL)"), JULIA("Julia"), XOJO("Xojo"); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PostgresqlSchemaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PostgresqlSchemaCodegen.java new file mode 100644 index 000000000000..5910be7699ab --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PostgresqlSchemaCodegen.java @@ -0,0 +1,1473 @@ +/* + * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.codegen.languages; + +import lombok.Getter; +import lombok.Setter; +import org.openapitools.codegen.*; +import org.openapitools.codegen.meta.GeneratorMetadata; +import org.openapitools.codegen.meta.Stability; +import org.openapitools.codegen.meta.features.*; +import org.openapitools.codegen.model.ModelMap; +import org.openapitools.codegen.model.ModelsMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.commons.lang3.StringUtils; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.io.File; + +import static org.openapitools.codegen.utils.StringUtils.underscore; + +@SuppressWarnings("unchecked") +public class PostgresqlSchemaCodegen extends DefaultCodegen { + private final Logger LOGGER = LoggerFactory.getLogger(PostgresqlSchemaCodegen.class); + + public static final String VENDOR_EXTENSION_POSTGRESQL_SCHEMA = "x-postgresql-schema"; + public static final String DEFAULT_DATABASE_NAME = "defaultDatabaseName"; + public static final String JSON_DATA_TYPE = "jsonDataType"; + public static final String IDENTIFIER_NAMING_CONVENTION = "identifierNamingConvention"; + public static final String NAMED_PARAMETERS_ENABLED = "namedParametersEnabled"; + public static final String ID_AUTOINC_ENABLED = "idAutoIncEnabled"; + public static final Integer ENUM_MAX_ELEMENTS = 65535; + public static final Integer IDENTIFIER_MAX_LENGTH = 63; + + protected Vector postgresqlNumericTypes = new Vector<>(Arrays.asList( + "SMALLINT", "INTEGER", "INT", "BIGINT", "DECIMAL", "NUMERIC", "REAL", "DOUBLE PRECISION", "SMALLSERIAL", + "SERIAL", + "BIGSERIAL")); + + protected Vector postgresqlDateAndTimeTypes = new Vector<>(Arrays.asList( + "DATE", "TIME", "TIME WITH TIME ZONE", "TIMESTAMP", "TIMESTAMP WITH TIME ZONE", "INTERVAL")); + + protected Vector postgresqlStringTypes = new Vector<>(Arrays.asList( + "CHAR", "VARCHAR", "TEXT")); + + protected Vector postgresqlSpatialTypes = new Vector<>(Arrays.asList( + "POINT", "LINE", "LSEG", "BOX", "PATH", "POLYGON", "CIRCLE")); + + /** + * Returns default database name for all PostgreSQL queries + * This value must be used with backticks only, e.g. `database_name` + */ + @Getter + protected String defaultDatabaseName = "", databaseNamePrefix = "", databaseNameSuffix = "_db"; + protected String tableNamePrefix = "tbl_", tableNameSuffix = ""; + protected String columnNamePrefix = "col_", columnNameSuffix = ""; + /** + * Which type of JSON data types will be used. + * JSON data type requires PostgreSQL version 9.4 or newer + */ + @Getter + @Setter + protected String jsonDataType = "json"; + /** + * Whether named parameters enabled or disabled in prepared SQLs + */ + @Getter + @Setter + protected Boolean namedParametersEnabled = false; + /** + * Returns identifier naming convention for table names and column names. + */ + @Getter + protected String identifierNamingConvention = "snake_case"; + /** + * Whether autoincrement feature enabled for integer 'id' fields + */ + @Getter + @Setter + protected Boolean idAutoIncEnabled = false; + + public PostgresqlSchemaCodegen() { + super(); + + generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata) + .stability(Stability.BETA) + .build(); + + modifyFeatureSet(features -> features + .includeDocumentationFeatures(DocumentationFeature.Readme) + .wireFormatFeatures(EnumSet.noneOf(WireFormatFeature.class)) + .securityFeatures(EnumSet.noneOf(SecurityFeature.class)) + .excludeGlobalFeatures( + GlobalFeature.XMLStructureDefinitions, + GlobalFeature.Callbacks, + GlobalFeature.LinkObjects, + GlobalFeature.ParameterStyling) + .excludeSchemaSupportFeatures( + SchemaSupportFeature.Polymorphism) + .clientModificationFeatures(EnumSet.noneOf(ClientModificationFeature.class))); + // clear import mapping (from default generator) as postgresql does not use + // import directives + importMapping.clear(); + + setModelPackage("Model"); + modelTemplateFiles.put("query_examples.mustache", ".sql"); + + // https://www.postgresql.org/docs/17/sql-keywords-appendix.html + setReservedWordsLowerCase( + Arrays.asList( + // SQL reserved words + "A", "ABORT", "ABS", "ABSENT", "ABSOLUTE", "ACCESS", "ACCORDING", "ACOS", "ACTION", "ADA", + "ADD", "ADMIN", "AFTER", "AGGREGATE", "ALL", "ALLOCATE", "ALSO", "ALTER", "ALWAYS", "ANALYSE", + "ANALYZE", "AND", "ANY", "ANY_VALUE", "ARE", "ARRAY", "ARRAY_AGG", "ARRAY_MAX_CARDINALITY", + "AS", "ASC", "ASENSITIVE", "ASIN", "ASSERTION", "ASSIGNMENT", "ASYMMETRIC", "AT", "ATAN", + "ATOMIC", "ATTACH", "ATTRIBUTE", "ATTRIBUTES", "AUTHORIZATION", "AVG", "BACKWARD", "BASE64", + "BEFORE", "BEGIN", "BEGIN_FRAME", "BEGIN_PARTITION", "BERNOULLI", "BETWEEN", "BIGINT", "BINARY", + "BIT", "BIT_LENGTH", "BLOB", "BLOCKED", "BOM", "BOOLEAN", "BOTH", "BREADTH", "BTRIM", "BY", "C", + "CACHE", "CALL", "CALLED", "CARDINALITY", "CASCADE", "CASCADED", "CASE", "CAST", "CATALOG", + "CATALOG_NAME", "CEIL", "CEILING", "CHAIN", "CHAINING", "CHAR", "CHARACTER", "CHARACTERISTICS", + "CHARACTERS", "CHARACTER_LENGTH", "CHARACTER_SET_CATALOG", "CHARACTER_SET_NAME", + "CHARACTER_SET_SCHEMA", "CHAR_LENGTH", "CHECK", "CHECKPOINT", "CLASS", "CLASSIFIER", + "CLASS_ORIGIN", "CLOB", "CLOSE", "CLUSTER", "COALESCE", "COBOL", "COLLATE", "COLLATION", + "COLLATION_CATALOG", "COLLATION_NAME", "COLLATION_SCHEMA", "COLLECT", "COLUMN", "COLUMNS", + "COLUMN_NAME", "COMMAND_FUNCTION", "COMMAND_FUNCTION_CODE", "COMMENT", "COMMENTS", "COMMIT", + "COMMITTED", "COMPRESSION", "CONCURRENTLY", "CONDITION", "CONDITIONAL", "CONDITION_NUMBER", + "CONFIGURATION", "CONFLICT", "CONNECT", "CONNECTION", "CONNECTION_NAME", "CONSTRAINT", + "CONSTRAINTS", "CONSTRAINT_CATALOG", "CONSTRAINT_NAME", "CONSTRAINT_SCHEMA", "CONSTRUCTOR", + "CONTAINS", "CONTENT", "CONTINUE", "CONTROL", "CONVERSION", "CONVERT", "COPARTITION", "COPY", + "CORR", "CORRESPONDING", "COS", "COSH", "COST", "COUNT", "COVAR_POP", "COVAR_SAMP", "CREATE", + "CROSS", "CSV", "CUBE", "CUME_DIST", "CURRENT", "CURRENT_CATALOG", "CURRENT_DATE", + "CURRENT_DEFAULT_TRANSFORM_GROUP", "CURRENT_PATH", "CURRENT_ROLE", "CURRENT_ROW", + "CURRENT_SCHEMA", "CURRENT_TIME", "CURRENT_TIMESTAMP", "CURRENT_TRANSFORM_GROUP_FOR_TYPE", + "CURRENT_USER", "CURSOR", "CURSOR_NAME", "CYCLE", "DATA", "DATABASE", "DATALINK", "DATE", + "DATETIME_INTERVAL_CODE", "DATETIME_INTERVAL_PRECISION", "DAY", "DB", "DEALLOCATE", "DEC", + "DECFLOAT", "DECIMAL", "DECLARE", "DEFAULT", "DEFAULTS", "DEFERRABLE", "DEFERRED", "DEFINE", + "DEFINED", "DEFINER", "DEGREE", "DELETE", "DELIMITER", "DELIMITERS", "DENSE_RANK", "DEPENDS", + "DEPTH", "DEREF", "DERIVED", "DESC", "DESCRIBE", "DESCRIPTOR", "DETACH", "DETERMINISTIC", + "DIAGNOSTICS", "DICTIONARY", "DISABLE", "DISCARD", "DISCONNECT", "DISPATCH", "DISTINCT", + "DLNEWCOPY", "DLPREVIOUSCOPY", "DLURLCOMPLETE", "DLURLCOMPLETEONLY", "DLURLCOMPLETEWRITE", + "DLURLPATH", "DLURLPATHONLY", "DLURLPATHWRITE", "DLURLSCHEME", "DLURLSERVER", "DLVALUE", "DO", + "DOCUMENT", "DOMAIN", "DOUBLE", "DROP", "DYNAMIC", "DYNAMIC_FUNCTION", "DYNAMIC_FUNCTION_CODE", + "EACH", "ELEMENT", "ELSE", "EMPTY", "ENABLE", "ENCODING", "ENCRYPTED", "END", "END-EXEC", + "END_FRAME", "END_PARTITION", "ENFORCED", "ENUM", "EQUALS", "ERROR", "ESCAPE", "EVENT", "EVERY", + "EXCEPT", "EXCEPTION", "EXCLUDE", "EXCLUDING", "EXCLUSIVE", "EXEC", "EXECUTE", "EXISTS", "EXP", + "EXPLAIN", "EXPRESSION", "EXTENSION", "EXTERNAL", "EXTRACT", "FALSE", "FAMILY", "FETCH", "FILE", + "FILTER", "FINAL", "FINALIZE", "FINISH", "FIRST", "FIRST_VALUE", "FLAG", "FLOAT", "FLOOR", + "FOLLOWING", "FOR", "FORCE", "FOREIGN", "FORMAT", "FORTRAN", "FORWARD", "FOUND", "FRAME_ROW", + "FREE", "FREEZE", "FROM", "FS", "FULFILL", "FULL", "FUNCTION", "FUNCTIONS", "FUSION", "G", + "GENERAL", "GENERATED", "GET", "GLOBAL", "GO", "GOTO", "GRANT", "GRANTED", "GREATEST", "GROUP", + "GROUPING", "GROUPS", "HANDLER", "HAVING", "HEADER", "HEX", "HIERARCHY", "HOLD", "HOUR", "ID", + "IDENTITY", "IF", "IGNORE", "ILIKE", "IMMEDIATE", "IMMEDIATELY", "IMMUTABLE", "IMPLEMENTATION", + "IMPLICIT", "IMPORT", "IN", "INCLUDE", "INCLUDING", "INCREMENT", "INDENT", "INDEX", "INDEXES", + "INDICATOR", "INHERIT", "INHERITS", "INITIAL", "INITIALLY", "INLINE", "INNER", "INOUT", "INPUT", + "INSENSITIVE", "INSERT", "INSTANCE", "INSTANTIABLE", "INSTEAD", "INT", "INTEGER", "INTEGRITY", + "INTERSECT", "INTERSECTION", "INTERVAL", "INTO", "INVOKER", "IS", "ISNULL", "ISOLATION", "JOIN", + "JSON", "JSON_ARRAY", "JSON_ARRAYAGG", "JSON_EXISTS", "JSON_OBJECT", "JSON_OBJECTAGG", + "JSON_QUERY", "JSON_SCALAR", "JSON_SERIALIZE", "JSON_TABLE", "JSON_TABLE_PRIMITIVE", + "JSON_VALUE", "K", "KEEP", "KEY", "KEYS", "KEY_MEMBER", "KEY_TYPE", "LABEL", "LAG", "LANGUAGE", + "LARGE", "LAST", "LAST_VALUE", "LATERAL", "LEAD", "LEADING", "LEAKPROOF", "LEAST", "LEFT", + "LENGTH", "LEVEL", "LIBRARY", "LIKE", "LIKE_REGEX", "LIMIT", "LINK", "LISTAGG", "LISTEN", "LN", + "LOAD", "LOCAL", "LOCALTIME", "LOCALTIMESTAMP", "LOCATION", "LOCATOR", "LOCK", "LOCKED", "LOG", + "LOG10", "LOGGED", "LOWER", "LPAD", "LTRIM", "M", "MAP", "MAPPING", "MATCH", "MATCHED", + "MATCHES", "MATCH_NUMBER", "MATCH_RECOGNIZE", "MATERIALIZED", "MAX", "MAXVALUE", "MEASURES", + "MEMBER", "MERGE", "MERGE_ACTION", "MESSAGE_LENGTH", "MESSAGE_OCTET_LENGTH", "MESSAGE_TEXT", + "METHOD", "MIN", "MINUTE", "MINVALUE", "MOD", "MODE", "MODIFIES", "MODULE", "MONTH", "MORE", + "MOVE", "MULTISET", "MUMPS", "NAME", "NAMES", "NAMESPACE", "NATIONAL", "NATURAL", "NCHAR", + "NCLOB", "NESTED", "NESTING", "NEW", "NEXT", "NFC", "NFD", "NFKC", "NFKD", "NIL", "NO", "NONE", + "NORMALIZE", "NORMALIZED", "NOT", "NOTHING", "NOTIFY", "NOTNULL", "NOWAIT", "NTH_VALUE", + "NTILE", "NULL", "NULLABLE", "NULLIF", "NULLS", "NULL_ORDERING", "NUMBER", "NUMERIC", "OBJECT", + "OCCURRENCE", "OCCURRENCES_REGEX", "OCTETS", "OCTET_LENGTH", "OF", "OFF", "OFFSET", "OIDS", + "OLD", "OMIT", "ON", "ONE", "ONLY", "OPEN", "OPERATOR", "OPTION", "OPTIONS", "OR", "ORDER", + "ORDERING", "ORDINALITY", "OTHERS", "OUT", "OUTER", "OUTPUT", "OVER", "OVERFLOW", "OVERLAPS", + "OVERLAY", "OVERRIDING", "OWNED", "OWNER", "P", "PAD", "PARALLEL", "PARAMETER", + "PARAMETER_MODE", "PARAMETER_NAME", "PARAMETER_ORDINAL_POSITION", "PARAMETER_SPECIFIC_CATALOG", + "PARAMETER_SPECIFIC_NAME", "PARAMETER_SPECIFIC_SCHEMA", "PARSER", "PARTIAL", "PARTITION", + "PASCAL", "PASS", "PASSING", "PASSTHROUGH", "PASSWORD", "PAST", "PATH", "PATTERN", "PER", + "PERCENT", "PERCENTILE_CONT", "PERCENTILE_DISC", "PERCENT_RANK", "PERIOD", "PERMISSION", + "PERMUTE", "PIPE", "PLACING", "PLAN", "PLANS", "PLI", "POLICY", "PORTION", "POSITION", + "POSITION_REGEX", "POWER", "PRECEDES", "PRECEDING", "PRECISION", "PREPARE", "PREPARED", + "PRESERVE", "PREV", "PRIMARY", "PRIOR", "PRIVATE", "PRIVILEGES", "PROCEDURAL", "PROCEDURE", + "PROCEDURES", "PROGRAM", "PRUNE", "PTF", "PUBLIC", "PUBLICATION", "QUOTE", "QUOTES", "RANGE", + "RANK", "READ", "READS", "REAL", "REASSIGN", "RECHECK", "RECOVERY", "RECURSIVE", "REF", + "REFERENCES", "REFERENCING", "REFRESH", "REGR_AVGX", "REGR_AVGY", "REGR_COUNT", + "REGR_INTERCEPT", "REGR_R2", "REGR_SLOPE", "REGR_SXX", "REGR_SXY", "REGR_SYY", "REINDEX", + "RELATIVE", "RELEASE", "RENAME", "REPEATABLE", "REPLACE", "REPLICA", "REQUIRING", "RESET", + "RESPECT", "RESTART", "RESTORE", "RESTRICT", "RESULT", "RETURN", "RETURNED_CARDINALITY", + "RETURNED_LENGTH", "RETURNED_OCTET_LENGTH", "RETURNED_SQLSTATE", "RETURNING", "RETURNS", + "REVOKE", "RIGHT", "ROLE", "ROLLBACK", "ROLLUP", "ROUTINE", "ROUTINES", "ROUTINE_CATALOG", + "ROUTINE_NAME", "ROUTINE_SCHEMA", "ROW", "ROWS", "ROW_COUNT", "ROW_NUMBER", "RPAD", "RTRIM", + "RULE", "RUNNING", "SAVEPOINT", "SCALAR", "SCALE", "SCHEMA", "SCHEMAS", "SCHEMA_NAME", "SCOPE", + "SCOPE_CATALOG", "SCOPE_NAME", "SCOPE_SCHEMA", "SCROLL", "SEARCH", "SECOND", "SECTION", + "SECURITY", "SEEK", "SELECT", "SELECTIVE", "SELF", "SEMANTICS", "SENSITIVE", "SEQUENCE", + "SEQUENCES", "SERIALIZABLE", "SERVER", "SERVER_NAME", "SESSION", "SESSION_USER", "SET", "SETOF", + "SETS", "SHARE", "SHOW", "SIMILAR", "SIMPLE", "SIN", "SINH", "SIZE", "SKIP", "SMALLINT", + "SNAPSHOT", "SOME", "SORT_DIRECTION", "SOURCE", "SPACE", "SPECIFIC", "SPECIFICTYPE", + "SPECIFIC_NAME", "SQL", "SQLCODE", "SQLERROR", "SQLEXCEPTION", "SQLSTATE", "SQLWARNING", "SQRT", + "STABLE", "STANDALONE", "START", "STATE", "STATEMENT", "STATIC", "STATISTICS", "STDDEV_POP", + "STDDEV_SAMP", "STDIN", "STDOUT", "STORAGE", "STORED", "STRICT", "STRING", "STRIP", "STRUCTURE", + "STYLE", "SUBCLASS_ORIGIN", "SUBMULTISET", "SUBSCRIPTION", "SUBSET", "SUBSTRING", + "SUBSTRING_REGEX", "SUCCEEDS", "SUM", "SUPPORT", "SYMMETRIC", "SYSID", "SYSTEM", "SYSTEM_TIME", + "SYSTEM_USER", "T", "TABLE", "TABLES", "TABLESAMPLE", "TABLESPACE", "TABLE_NAME", "TAN", "TANH", + "TARGET", "TEMP", "TEMPLATE", "TEMPORARY", "TEXT", "THEN", "THROUGH", "TIES", "TIME", + "TIMESTAMP", "TIMEZONE_HOUR", "TIMEZONE_MINUTE", "TO", "TOKEN", "TOP_LEVEL_COUNT", "TRAILING", + "TRANSACTION", "TRANSACTIONS_COMMITTED", "TRANSACTIONS_ROLLED_BACK", "TRANSACTION_ACTIVE", + "TRANSFORM", "TRANSFORMS", "TRANSLATE", "TRANSLATE_REGEX", "TRANSLATION", "TREAT", "TRIGGER", + "TRIGGER_CATALOG", "TRIGGER_NAME", "TRIGGER_SCHEMA", "TRIM", "TRIM_ARRAY", "TRUE", "TRUNCATE", + "TRUSTED", "TYPE", "TYPES", "UESCAPE", "UNBOUNDED", "UNCOMMITTED", "UNCONDITIONAL", "UNDER", + "UNENCRYPTED", "UNION", "UNIQUE", "UNKNOWN", "UNLINK", "UNLISTEN", "UNLOGGED", "UNMATCHED", + "UNNAMED", "UNNEST", "UNTIL", "UNTYPED", "UPDATE", "UPPER", "URI", "USAGE", "USER", + "USER_DEFINED_TYPE_CATALOG", "USER_DEFINED_TYPE_CODE", "USER_DEFINED_TYPE_NAME", + "USER_DEFINED_TYPE_SCHEMA", "USING", "UTF16", "UTF32", "UTF8", "VACUUM", "VALID", "VALIDATE", + "VALIDATOR", "VALUE", "VALUES", "VALUE_OF", "VARBINARY", "VARCHAR", "VARIADIC", "VARYING", + "VAR_POP", "VAR_SAMP", "VERBOSE", "VERSION", "VERSIONING", "VIEW", "VIEWS", "VOLATILE", "WHEN", + "WHENEVER", "WHERE", "WHITESPACE", "WIDTH_BUCKET", "WINDOW", "WITH", "WITHIN", "WITHOUT", + "WORK", "WRAPPER", "WRITE", "XML", "XMLAGG", "XMLATTRIBUTES", "XMLBINARY", "XMLCAST", + "XMLCOMMENT", "XMLCONCAT", "XMLDECLARATION", "XMLDOCUMENT", "XMLELEMENT", "XMLEXISTS", + "XMLFOREST", "XMLITERATE", "XMLNAMESPACES", "XMLPARSE", "XMLPI", "XMLQUERY", "XMLROOT", + "XMLSCHEMA", "XMLSERIALIZE", "XMLTABLE", "XMLTEXT", "XMLVALIDATE", "YEAR", "YES", "ZONE")); + + // primitive data types + languageSpecificPrimitives = new HashSet<>( + Arrays.asList( + "bool", + "boolean", + "int", + "integer", + "double", + "float", + "string", + "date", + "Date", + "DateTime", + "long", + "short", + "char", + "ByteArray", + "file", + "UUID", + "URI", + "BigDecimal", + "mixed", + "number", + "void", + "byte")); + + // https://www.postgresql.org/docs/17/datatype.html + typeMapping.put("array", "JSON"); + typeMapping.put("set", "JSON"); + typeMapping.put("map", "JSON"); + typeMapping.put("List", "JSON"); + typeMapping.put("boolean", "BOOLEAN"); + typeMapping.put("string", "TEXT"); + typeMapping.put("int", "INTEGER"); + typeMapping.put("byte", "TEXT"); + typeMapping.put("float", "DECIMAL"); + typeMapping.put("number", "DECIMAL"); + typeMapping.put("date", "DATE"); + typeMapping.put("DateTime", "TIMESTAMP"); + typeMapping.put("long", "BIGINT"); + typeMapping.put("short", "SMALLINT"); + typeMapping.put("char", "TEXT"); + typeMapping.put("double", "DECIMAL"); + typeMapping.put("object", "JSON"); + typeMapping.put("integer", "INTEGER"); + typeMapping.put("ByteArray", "BYTEA"); + typeMapping.put("file", "BYTEA"); + typeMapping.put("UUID", "TEXT"); + typeMapping.put("URI", "TEXT"); + typeMapping.put("BigDecimal", "DECIMAL"); + + embeddedTemplateDir = templateDir = "postgresql-schema"; + + // it seems that cli options from DefaultCodegen are useless here + cliOptions.clear(); + + addOption(DEFAULT_DATABASE_NAME, + "Database name that will be used for all generated PostgreSQL DDL and DML statements.", defaultDatabaseName); + + addSwitch(NAMED_PARAMETERS_ENABLED, + "Generates query examples with named variables in value placeholders (eg.`:name`,`:quantity`) if `true`. Otherwise, generates question marks `?` in value placeholders.", + namedParametersEnabled); + + addSwitch(ID_AUTOINC_ENABLED, + "If `true`, generates autoincrement PostgreSQL types `SERIAL` and `BIGSERIAL` for `int32` and `int64` respectively for integer fields with name 'id'.", + idAutoIncEnabled); + + // we used to snake_case table/column names, let's add this option + CliOption identifierNamingOpt = new CliOption(IDENTIFIER_NAMING_CONVENTION, + "Naming convention of PostgreSQL idebntifiers (table names and column names)."); + identifierNamingOpt.addEnum("snake_case", "Transform named to 'snake_case'.") + .addEnum("original", "Leave original names as in `YAML` file.") + .setDefault("snake_case"); + cliOptions.add(identifierNamingOpt); + + CliOption jsonDataTypeOpt = new CliOption(JSON_DATA_TYPE, + "Use of PostgreSQL data types for complex model properties."); + jsonDataTypeOpt.addEnum("json", "Generate `JSON` fields. Value is stored in `JSON` data type field as human-readable text. Value compliance with JSON standard is checked.") + .addEnum("jsonb", + "Generate `JSONB` fields. Value is stored in `JSONB` data type field in binary format. `JSONB` data type is generally nore efficient than `JSON` but it is not human-readable. Value compliance with JSON standard is checked.") + .addEnum("off", "Generate `TEXT` fields. Just store the value as plain text. Value compliance with JSON standard is not checked.") + .setDefault("json"); + cliOptions.add(jsonDataTypeOpt); + } + + @Override + public CodegenType getTag() { + return CodegenType.SCHEMA; + } + + @Override + public String getName() { + return "postgresql-schema"; + } + + @Override + public String getHelp() { + return "Generates a PostgreSQL schema based on the schema defined in the OpenAPI specification (v2, v3)"; + } + + @Override + public void processOpts() { + super.processOpts(); + + if (additionalProperties.containsKey(DEFAULT_DATABASE_NAME)) { + if (additionalProperties.get(DEFAULT_DATABASE_NAME).equals("")) { + additionalProperties.remove(DEFAULT_DATABASE_NAME); + } else { + this.setDefaultDatabaseName((String) additionalProperties.get(DEFAULT_DATABASE_NAME)); + // default database name may be escaped, need to overwrite additional prop + additionalProperties.put(DEFAULT_DATABASE_NAME, getDefaultDatabaseName()); + } + } + + if (additionalProperties.containsKey(NAMED_PARAMETERS_ENABLED)) { + this.setNamedParametersEnabled( + Boolean.valueOf(additionalProperties.get(NAMED_PARAMETERS_ENABLED).toString())); + } + additionalProperties.put(NAMED_PARAMETERS_ENABLED, getNamedParametersEnabled()); + + if (additionalProperties.containsKey(ID_AUTOINC_ENABLED)) { + this.setIdAutoIncEnabled( + Boolean.valueOf(additionalProperties.get(ID_AUTOINC_ENABLED).toString())); + } + additionalProperties.put(ID_AUTOINC_ENABLED, getIdAutoIncEnabled()); + + if (additionalProperties.containsKey(IDENTIFIER_NAMING_CONVENTION)) { + this.setIdentifierNamingConvention((String) additionalProperties.get(IDENTIFIER_NAMING_CONVENTION)); + } + + if (additionalProperties.containsKey(JSON_DATA_TYPE)) { + this.setJsonDataType((String) additionalProperties.get(JSON_DATA_TYPE)); + } + + // make model src path available in mustache template + additionalProperties.put("modelSrcPath", "./" + toSrcPath(modelPackage)); + + supportingFiles.add(new SupportingFile( + "README.mustache", "", "README.md")); + supportingFiles.add(new SupportingFile( + "postgresql_schema.mustache", "", "postgresql_schema.sql")); + supportingFiles.add(new SupportingFile( + "postgresql_schema_oauth2.mustache", "", "postgresql_schema_oauth2.sql")); + } + + @Override + public ModelsMap postProcessModels(ModelsMap objs) { + objs = super.postProcessModels(objs); + + for (ModelMap mo : objs.getModels()) { + CodegenModel model = mo.getModel(); + String modelName = model.getName(); + String tableName = this.toTableName(modelName); + String modelDescription = model.getDescription(); + Map modelVendorExtensions = model.getVendorExtensions(); + Map postgresqlSchema = new HashMap<>(); + Map tableDefinition = new HashMap<>(); + + if (this.getIdentifierNamingConvention().equals("snake_case") && !modelName.equals(tableName)) { + // add original name in table comment + String commentExtra = "Original model name - " + modelName + "."; + modelDescription = (modelDescription == null || modelDescription.isEmpty()) ? commentExtra + : modelDescription + ". " + commentExtra; + } + + if (modelVendorExtensions.containsKey(VENDOR_EXTENSION_POSTGRESQL_SCHEMA)) { + // user already specified schema values + LOGGER.info("Found vendor extension in '{}' model, autogeneration skipped", modelName); + } else { + modelVendorExtensions.put(VENDOR_EXTENSION_POSTGRESQL_SCHEMA, postgresqlSchema); + postgresqlSchema.put("tableDefinition", tableDefinition); + tableDefinition.put("tblName", tableName); + tableDefinition.put("tblComment", modelDescription); + if (isReservedWord(tableName)) { // Output table name in double quotes if it is a reserved word + tableDefinition.put("tblNameQuoted", true); + } + } + } + return objs; + } + + @Override + public void postProcessModelProperty(CodegenModel model, CodegenProperty property) { + switch (property.getDataType().toUpperCase(Locale.ROOT)) { + case "BOOLEAN": + processBooleanTypeProperty(model, property); + break; + case "SMALLINT": + case "INTEGER": + case "BIGINT": + processIntegerTypeProperty(model, property); + break; + case "DECIMAL": + processDecimalTypeProperty(model, property); + break; + case "TEXT": + case "BYTEA": + processStringTypeProperty(model, property); + break; + case "DATE": + case "TIMESTAMP": + processDateTypeProperty(model, property); + break; + case "JSON": + case "JSONB": + processJsonTypeProperty(model, property); + break; + default: + processUnknownTypeProperty(model, property); + } + } + + /** + * Processes each model's property mapped to integer type and adds related + * vendor extensions + * + * @param model model + * @param property model's property + */ + public void processIntegerTypeProperty(CodegenModel model, CodegenProperty property) { + Map vendorExtensions = property.getVendorExtensions(); + Map postgresqlSchema = new HashMap<>(); + Map columnDefinition = new HashMap<>(); + Map typeDefinition = new HashMap<>(); + ArrayList columnDataTypeArguments = new ArrayList(); + String baseName = property.getBaseName(); + String colName = this.toColumnName(baseName); + String dataType = property.getDataType(); + String dataFormat = property.getDataFormat(); + String description = property.getDescription(); + String minimum = property.getMinimum(); + String maximum = property.getMaximum(); + boolean exclusiveMinimum = property.getExclusiveMinimum(); + boolean exclusiveMaximum = property.getIExclusiveMaximum(); + String defaultValue = property.getDefaultValue(); + Boolean required = property.getRequired(); + Boolean isUuid = property.isUuid; + Boolean isEnum = property.isEnum; + String tableName = this.toTableName(model.getName()); + + if (vendorExtensions.containsKey(VENDOR_EXTENSION_POSTGRESQL_SCHEMA)) { + // user already specified schema values + LOGGER.info("Found vendor extension in '{}' property, autogeneration skipped", baseName); + return; + } + + if (this.getIdentifierNamingConvention().equals("snake_case") && !baseName.equals(colName)) { + // add original name in column comment + String commentExtra = "Original param name - " + baseName + "."; + description = (description == null || description.isEmpty()) ? commentExtra + : description + ". " + commentExtra; + } + + vendorExtensions.put(VENDOR_EXTENSION_POSTGRESQL_SCHEMA, postgresqlSchema); + postgresqlSchema.put("columnDefinition", columnDefinition); + columnDefinition.put("colName", colName); + if (isReservedWord(colName)) { // Output column name in double quotes if it is a reserved word + columnDefinition.put("colNameQuoted", true); + } else { + columnDefinition.put("colNameQuoted", false); + } + columnDefinition.put("tblName", tableName); + if (isReservedWord(model.getName())) { // Output table name (for column comment) in double quotes if it is a + // reserved word + columnDefinition.put("tblNameQuoted", true); + } else { + columnDefinition.put("tblNameQuoted", false); + } + + if (Boolean.TRUE.equals(isEnum)) { + Map allowableValues = property.getAllowableValues(); + List enumValues = (List) allowableValues.get("values"); + String typeName = this.toTableName(model.getName()) + + "_" + this.toColumnName(property.getName()); + postgresqlSchema.put("typeDefinition", typeDefinition); + columnDefinition.put("colDataType", typeName); + typeDefinition.put("typeName", typeName); + typeDefinition.put("typeArguments", columnDataTypeArguments); + for (int i = 0; i < enumValues.size(); i++) { + if (i > ENUM_MAX_ELEMENTS - 1) { + LOGGER.warn( + "ENUM column can have maximum of {} distinct elements, following value will be skipped: {}", + ENUM_MAX_ELEMENTS, (String) enumValues.get(i)); + break; + } + String value = String.valueOf(enumValues.get(i)); + columnDataTypeArguments.add(toCodegenPostgresqlDataTypeArgument(value)); + } + } else { + if ("int64".equals(dataFormat)) { + if (colName.equals("id") && Boolean.valueOf(additionalProperties.get(ID_AUTOINC_ENABLED).toString())) { + columnDefinition.put("colDataType", "BIGSERIAL"); + } else { + columnDefinition.put("colDataType", "BIGINT"); + } + } else { + Long min = (minimum != null) ? Long.parseLong(minimum) : null; + Long max = (maximum != null) ? Long.parseLong(maximum) : null; + if (exclusiveMinimum && min != null) + min += 1; + String colDataType = getPostgresqlMatchedIntegerDataType(min, max, false); + if (colName.equals("id") && Boolean.valueOf(additionalProperties.get(ID_AUTOINC_ENABLED).toString())) { + if (colDataType.equals("BIGINT")) { + colDataType = "BIGSERIAL"; + } else { + colDataType = "SERIAL"; + } + } + columnDefinition.put("colDataType", colDataType); + } + } + + if (!columnDefinition.get("colDataType").equals("SERIAL") + && !columnDefinition.get("colDataType").equals("BIGSERIAL")) { // No default value for autoincremented + // IDs + if (Boolean.TRUE.equals(required)) { + columnDefinition.put("colNotNull", true); + } else { + columnDefinition.put("colNotNull", false); + try { + columnDefinition.put("colDefault", + toCodegenPostgresqlDataTypeDefault(defaultValue, + (String) columnDefinition.get("colDataType"))); + } catch (RuntimeException exception) { + LOGGER.warn( + "Property '{}' of model '{}' mapped to PostgreSQL data type which doesn't support default value", + baseName, model.getName()); + columnDefinition.put("colDefault", null); + } + } + } + + if (description != null) { + columnDefinition.put("colComment", description); + } + } + + /** + * Processes each model's property mapped to decimal type and adds related + * vendor extensions + * + * @param model model + * @param property model's property + */ + public void processDecimalTypeProperty(CodegenModel model, CodegenProperty property) { + Map vendorExtensions = property.getVendorExtensions(); + Map postgresqlSchema = new HashMap<>(); + Map columnDefinition = new HashMap<>(); + Map typeDefinition = new HashMap<>(); + ArrayList columnDataTypeArguments = new ArrayList(); + String baseName = property.getBaseName(); + String colName = this.toColumnName(baseName); + String dataType = property.getDataType(); + String dataFormat = property.getDataFormat(); + String description = property.getDescription(); + String minimum = property.getMinimum(); + String maximum = property.getMaximum(); + boolean exclusiveMinimum = property.getExclusiveMinimum(); + boolean exclusiveMaximum = property.getIExclusiveMaximum(); + String defaultValue = property.getDefaultValue(); + Boolean required = property.getRequired(); + Boolean isEnum = property.isEnum; + String tableName = this.toTableName(model.getName()); + + if (vendorExtensions.containsKey(VENDOR_EXTENSION_POSTGRESQL_SCHEMA)) { + // user already specified schema values + LOGGER.info("Found vendor extension in '{}' property, autogeneration skipped", baseName); + return; + } + + if (this.getIdentifierNamingConvention().equals("snake_case") && !baseName.equals(colName)) { + // add original name in column comment + String commentExtra = "Original param name - " + baseName + "."; + description = (description == null || description.isEmpty()) ? commentExtra + : description + ". " + commentExtra; + } + + vendorExtensions.put(VENDOR_EXTENSION_POSTGRESQL_SCHEMA, postgresqlSchema); + postgresqlSchema.put("columnDefinition", columnDefinition); + columnDefinition.put("colName", colName); + if (isReservedWord(colName)) { // Output column name in double quotes if it is a reserved word + columnDefinition.put("colNameQuoted", true); + } else { + columnDefinition.put("colNameQuoted", false); + } + columnDefinition.put("tblName", tableName); + if (isReservedWord(model.getName())) { // Output table name (for column comment) in double quotes if it is a + // reserved word + columnDefinition.put("tblNameQuoted", true); + } else { + columnDefinition.put("tblNameQuoted", false); + } + + if (Boolean.TRUE.equals(isEnum)) { + Map allowableValues = property.getAllowableValues(); + List enumValues = (List) allowableValues.get("values"); + String typeName = this.toTableName(model.getName()) + + "_" + this.toColumnName(property.getName()); + postgresqlSchema.put("typeDefinition", typeDefinition); + columnDefinition.put("colDataType", typeName); + typeDefinition.put("typeName", typeName); + typeDefinition.put("typeArguments", columnDataTypeArguments); + for (int i = 0; i < enumValues.size(); i++) { + if (i > ENUM_MAX_ELEMENTS - 1) { + LOGGER.warn( + "ENUM column can have maximum of {} distinct elements, following value will be skipped: {}", + ENUM_MAX_ELEMENTS, (String) enumValues.get(i)); + break; + } + String value = String.valueOf(enumValues.get(i)); + columnDataTypeArguments.add(toCodegenPostgresqlDataTypeArgument(value)); + } + } else { + Float min = (minimum != null) ? Float.valueOf(minimum) : null; + Float max = (maximum != null) ? Float.valueOf(maximum) : null; + if (exclusiveMinimum && min != null) + min += 1; + if (exclusiveMaximum && max != null) + max -= 1; + columnDefinition.put("colDataType", "DECIMAL"); + columnDefinition.put("colDataTypeArguments", columnDataTypeArguments); + columnDataTypeArguments.add(toCodegenPostgresqlDataTypeArgument(20)); + columnDataTypeArguments.add(toCodegenPostgresqlDataTypeArgument(9)); + } + + if (Boolean.TRUE.equals(required)) { + columnDefinition.put("colNotNull", true); + } else { + columnDefinition.put("colNotNull", false); + try { + columnDefinition.put("colDefault", + toCodegenPostgresqlDataTypeDefault(defaultValue, (String) columnDefinition.get("colDataType"))); + } catch (RuntimeException exception) { + LOGGER.warn( + "Property '{}' of model '{}' mapped to PostgreSQL data type which doesn't support default value", + baseName, model.getName()); + columnDefinition.put("colDefault", null); + } + } + + if (description != null) { + columnDefinition.put("colComment", description); + } + } + + /** + * Processes each model's property mapped to string type and adds related vendor + * extensions + * + * @param model model + * @param property model's property + */ + public void processStringTypeProperty(CodegenModel model, CodegenProperty property) { + Map vendorExtensions = property.getVendorExtensions(); + Map postgresqlSchema = new HashMap<>(); + Map columnDefinition = new HashMap<>(); + Map typeDefinition = new HashMap<>(); + ArrayList columnDataTypeArguments = new ArrayList(); + String baseName = property.getBaseName(); + String colName = this.toColumnName(baseName); + String dataType = property.getDataType(); + String dataFormat = property.getDataFormat(); + String description = property.getDescription(); + Integer minLength = property.getMinLength(); + Integer maxLength = property.getMaxLength(); + String defaultValue = property.getDefaultValue(); + Boolean required = property.getRequired(); + Boolean isEnum = property.isEnum; + String tableName = this.toTableName(model.getName()); + + if (vendorExtensions.containsKey(VENDOR_EXTENSION_POSTGRESQL_SCHEMA)) { + // user already specified schema values + LOGGER.info("Found vendor extension in '{}' property, autogeneration skipped", baseName); + return; + } + + if (this.getIdentifierNamingConvention().equals("snake_case") && !baseName.equals(colName)) { + // add original name in column comment + String commentExtra = "Original param name - " + baseName + "."; + description = (description == null || description.isEmpty()) ? commentExtra + : description + ". " + commentExtra; + } + + vendorExtensions.put(VENDOR_EXTENSION_POSTGRESQL_SCHEMA, postgresqlSchema); + postgresqlSchema.put("columnDefinition", columnDefinition); + columnDefinition.put("colName", colName); + if (isReservedWord(colName)) { // Output column name in double quotes if it is a reserved word + columnDefinition.put("colNameQuoted", true); + } else { + columnDefinition.put("colNameQuoted", false); + } + columnDefinition.put("tblName", tableName); + if (isReservedWord(model.getName())) { // Output table name (for column comment) in double quotes if it is a + // reserved word + columnDefinition.put("tblNameQuoted", true); + } else { + columnDefinition.put("tblNameQuoted", false); + } + + if (Boolean.TRUE.equals(isEnum)) { + Map allowableValues = property.getAllowableValues(); + List enumValues = (List) allowableValues.get("values"); + String typeName = this.toTableName(model.getName()) + + "_" + this.toColumnName(property.getName()); + postgresqlSchema.put("typeDefinition", typeDefinition); + columnDefinition.put("colDataType", typeName); + typeDefinition.put("typeName", typeName); + typeDefinition.put("typeArguments", columnDataTypeArguments); + for (int i = 0; i < enumValues.size(); i++) { + if (i > ENUM_MAX_ELEMENTS - 1) { + LOGGER.warn( + "ENUM column can have maximum of {} distinct elements, following value will be skipped: {}", + ENUM_MAX_ELEMENTS, (String) enumValues.get(i)); + break; + } + String value = String.valueOf(enumValues.get(i)); + columnDataTypeArguments.add(toCodegenPostgresqlDataTypeArgument(value)); + } + } else if (dataType.equals("BYTEA")) { + columnDefinition.put("colDataType", "BYTEA"); + } else { + String matchedStringType = getPostgresqlMatchedStringDataType(minLength, maxLength); + columnDefinition.put("colDataType", matchedStringType); + if (matchedStringType.equals("VARCHAR")) { + columnDefinition.put("colDataTypeArguments", columnDataTypeArguments); + columnDataTypeArguments.add(toCodegenPostgresqlDataTypeArgument((maxLength != null) ? maxLength : 255)); + } + } + + if (Boolean.TRUE.equals(required)) { + columnDefinition.put("colNotNull", true); + } else { + columnDefinition.put("colNotNull", false); + try { + columnDefinition.put("colDefault", + toCodegenPostgresqlDataTypeDefault(defaultValue, (String) columnDefinition.get("colDataType"))); + } catch (RuntimeException exception) { + LOGGER.warn( + "Property '{}' of model '{}' mapped to PostgreSQL data type which doesn't support default value", + baseName, model.getName()); + columnDefinition.put("colDefault", null); + } + } + + if (description != null) { + columnDefinition.put("colComment", description); + } + + } + + /** + * Processes each model's property mapped to boolean type and adds related + * vendor extensions + * + * @param model model + * @param property model's property + */ + public void processBooleanTypeProperty(CodegenModel model, CodegenProperty property) { + Map vendorExtensions = property.getVendorExtensions(); + Map postgresqlSchema = new HashMap<>(); + Map columnDefinition = new HashMap<>(); + ArrayList columnDataTypeArguments = new ArrayList(); + String baseName = property.getBaseName(); + String colName = this.toColumnName(baseName); + String description = property.getDescription(); + String defaultValue = property.getDefaultValue(); + Boolean required = property.getRequired(); + String tableName = this.toTableName(model.getName()); + + if (vendorExtensions.containsKey(VENDOR_EXTENSION_POSTGRESQL_SCHEMA)) { + // user already specified schema values + LOGGER.info("Found vendor extension in '{}' property, autogeneration skipped", baseName); + return; + } + + if (this.getIdentifierNamingConvention().equals("snake_case") && !baseName.equals(colName)) { + // add original name in column comment + String commentExtra = "Original param name - " + baseName + "."; + description = (description == null || description.isEmpty()) ? commentExtra + : description + ". " + commentExtra; + } + + vendorExtensions.put(VENDOR_EXTENSION_POSTGRESQL_SCHEMA, postgresqlSchema); + postgresqlSchema.put("columnDefinition", columnDefinition); + columnDefinition.put("colName", colName); + if (isReservedWord(colName)) { // Output column name in double quotes if it is a reserved word + columnDefinition.put("colNameQuoted", true); + } else { + columnDefinition.put("colNameQuoted", false); + } + columnDefinition.put("tblName", tableName); + if (isReservedWord(model.getName())) { // Output table name (for column comment) in double quotes if it is a + // reserved word + columnDefinition.put("tblNameQuoted", true); + } else { + columnDefinition.put("tblNameQuoted", false); + } + + columnDefinition.put("colDataType", "BOOLEAN"); + + if (Boolean.TRUE.equals(required)) { + columnDefinition.put("colNotNull", true); + } else { + columnDefinition.put("colNotNull", false); + try { + columnDefinition.put("colDefault", + toCodegenPostgresqlDataTypeDefault(defaultValue, (String) columnDefinition.get("colDataType"))); + } catch (RuntimeException exception) { + LOGGER.warn( + "Property '{}' of model '{}' mapped to PostgreSQL data type which doesn't support default value", + baseName, model.getName()); + columnDefinition.put("colDefault", null); + } + } + + if (description != null) { + columnDefinition.put("colComment", description); + } + } + + /** + * Processes each model's property mapped to date type and adds related vendor + * extensions + * + * @param model model + * @param property model's property + */ + public void processDateTypeProperty(CodegenModel model, CodegenProperty property) { + Map vendorExtensions = property.getVendorExtensions(); + Map postgresqlSchema = new HashMap<>(); + Map columnDefinition = new HashMap<>(); + String baseName = property.getBaseName(); + String colName = this.toColumnName(baseName); + String dataType = property.getDataType(); + Boolean required = property.getRequired(); + String description = property.getDescription(); + String defaultValue = property.getDefaultValue(); + String tableName = this.toTableName(model.getName()); + + if (vendorExtensions.containsKey(VENDOR_EXTENSION_POSTGRESQL_SCHEMA)) { + // user already specified schema values + LOGGER.info("Found vendor extension in '{}' property, autogeneration skipped", baseName); + return; + } + + if (this.getIdentifierNamingConvention().equals("snake_case") && !baseName.equals(colName)) { + // add original name in column comment + String commentExtra = "Original param name - " + baseName + "."; + description = (description == null || description.isEmpty()) ? commentExtra + : description + ". " + commentExtra; + } + + vendorExtensions.put(VENDOR_EXTENSION_POSTGRESQL_SCHEMA, postgresqlSchema); + postgresqlSchema.put("columnDefinition", columnDefinition); + columnDefinition.put("colName", colName); + if (isReservedWord(colName)) { // Output column name in double quotes if it is a reserved word + columnDefinition.put("colNameQuoted", true); + } else { + columnDefinition.put("colNameQuoted", false); + } + columnDefinition.put("tblName", tableName); + if (isReservedWord(model.getName())) { // Output table name (for column comment) in double quotes if it is a + // reserved word + columnDefinition.put("tblNameQuoted", true); + } else { + columnDefinition.put("tblNameQuoted", false); + } + + columnDefinition.put("colDataType", dataType); + + if (Boolean.TRUE.equals(required)) { + columnDefinition.put("colNotNull", true); + } else { + columnDefinition.put("colNotNull", false); + try { + columnDefinition.put("colDefault", + toCodegenPostgresqlDataTypeDefault(defaultValue, (String) columnDefinition.get("colDataType"))); + } catch (RuntimeException exception) { + LOGGER.warn( + "Property '{}' of model '{}' mapped to PostgreSQL data type which doesn't support default value", + baseName, model.getName()); + columnDefinition.put("colDefault", null); + } + } + + if (description != null) { + columnDefinition.put("colComment", description); + } + } + + /** + * Processes each model's property mapped to JSON type and adds related vendor + * extensions + * + * @param model model + * @param property model's property + */ + public void processJsonTypeProperty(CodegenModel model, CodegenProperty property) { + Map vendorExtensions = property.getVendorExtensions(); + Map postgresqlSchema = new HashMap<>(); + Map columnDefinition = new HashMap<>(); + String baseName = property.getBaseName(); + String colName = this.toColumnName(baseName); + String dataType = property.getDataType(); + Boolean required = property.getRequired(); + String description = property.getDescription(); + String defaultValue = property.getDefaultValue(); + String tableName = this.toTableName(model.getName()); + + if (vendorExtensions.containsKey(VENDOR_EXTENSION_POSTGRESQL_SCHEMA)) { + // user already specified schema values + LOGGER.info("Found vendor extension in '{}' property, autogeneration skipped", baseName); + return; + } + + if (this.getIdentifierNamingConvention().equals("snake_case") && !baseName.equals(colName)) { + // add original name in column comment + String commentExtra = "Original param name - " + baseName + "."; + description = (description == null || description.isEmpty()) ? commentExtra + : description + ". " + commentExtra; + } + + vendorExtensions.put(VENDOR_EXTENSION_POSTGRESQL_SCHEMA, postgresqlSchema); + postgresqlSchema.put("columnDefinition", columnDefinition); + columnDefinition.put("colName", colName); + if (isReservedWord(colName)) { // Output column name in double quotes if it is a reserved word + columnDefinition.put("colNameQuoted", true); + } else { + columnDefinition.put("colNameQuoted", false); + } + columnDefinition.put("tblName", tableName); + if (isReservedWord(model.getName())) { // Output table name (for column comment) in double quotes if it is a + // reserved word + columnDefinition.put("tblNameQuoted", true); + } else { + columnDefinition.put("tblNameQuoted", false); + } + + columnDefinition.put("colDataType", dataType); + + if (getJsonDataType().equals("off")) { + columnDefinition.put("colDataType", "TEXT"); + } else if (getJsonDataType().equals("json")) { + columnDefinition.put("colDataType", "JSON"); + } else if (getJsonDataType().equals("jsonb")) { + columnDefinition.put("colDataType", "JSONB"); + } + + if (Boolean.TRUE.equals(required)) { + columnDefinition.put("colNotNull", true); + } else { + columnDefinition.put("colNotNull", false); + try { + columnDefinition.put("colDefault", + toCodegenPostgresqlDataTypeDefault(defaultValue, (String) columnDefinition.get("colDataType"))); + } catch (RuntimeException exception) { + LOGGER.warn( + "Property '{}' of model '{}' mapped to PostgreSQL data type which doesn't support default value", + baseName, model.getName()); + columnDefinition.put("colDefault", null); + } + } + + if (description != null) { + columnDefinition.put("colComment", description); + } + } + + /** + * Processes each model's property not mapped to any type and adds related + * vendor extensions + * Most of time it's related to referenced properties eg. \Model\User + * + * @param model model + * @param property model's property + */ + public void processUnknownTypeProperty(CodegenModel model, CodegenProperty property) { + Map vendorExtensions = property.getVendorExtensions(); + Map postgresqlSchema = new HashMap<>(); + Map columnDefinition = new HashMap<>(); + String baseName = property.getBaseName(); + String colName = this.toColumnName(baseName); + Boolean required = property.getRequired(); + String description = property.getDescription(); + String defaultValue = property.getDefaultValue(); + String tableName = this.toTableName(model.getName()); + + if (vendorExtensions.containsKey(VENDOR_EXTENSION_POSTGRESQL_SCHEMA)) { + // user already specified schema values + LOGGER.info("Found vendor extension in '{}' property, autogeneration skipped", baseName); + return; + } + + if (this.getIdentifierNamingConvention().equals("snake_case") && !baseName.equals(colName)) { + // add original name in column comment + String commentExtra = "Original param name - " + baseName + "."; + description = (description == null || description.isEmpty()) ? commentExtra + : description + ". " + commentExtra; + } + + vendorExtensions.put(VENDOR_EXTENSION_POSTGRESQL_SCHEMA, postgresqlSchema); + postgresqlSchema.put("columnDefinition", columnDefinition); + columnDefinition.put("colName", colName); + if (isReservedWord(colName)) { // Output column name in double quotes if it is a reserved word + columnDefinition.put("colNameQuoted", true); + } else { + columnDefinition.put("colNameQuoted", false); + } + columnDefinition.put("tblName", tableName); + if (isReservedWord(model.getName())) { // Output table name (for column comment) in double quotes if it is a + // reserved word + columnDefinition.put("tblNameQuoted", true); + } else { + columnDefinition.put("tblNameQuoted", false); + } + + columnDefinition.put("colDataType", "TEXT"); + + if (Boolean.TRUE.equals(required)) { + columnDefinition.put("colNotNull", true); + } else { + columnDefinition.put("colNotNull", false); + try { + columnDefinition.put("colDefault", + toCodegenPostgresqlDataTypeDefault(defaultValue, (String) columnDefinition.get("colDataType"))); + } catch (RuntimeException exception) { + LOGGER.warn( + "Property '{}' of model '{}' mapped to PostgreSQL data type which doesn't support default value", + baseName, model.getName()); + columnDefinition.put("colDefault", null); + } + } + + if (description != null) { + columnDefinition.put("colComment", description); + } + } + + /** + * Generates codegen property for PostgreSQL data type argument + * + * @param value argument value + * @return generated codegen property + */ + public HashMap toCodegenPostgresqlDataTypeArgument(Object value) { + HashMap arg = new HashMap<>(); + if (value instanceof String) { + arg.put("isString", true); + arg.put("isFloat", false); + arg.put("isInteger", false); + arg.put("isNumeric", false); + } else if (value instanceof Integer || value instanceof Long) { + arg.put("isString", false); + arg.put("isFloat", false); + arg.put("isInteger", true); + arg.put("isNumeric", true); + } else if (value instanceof Number) { + arg.put("isString", false); + arg.put("isFloat", true); + arg.put("isInteger", false); + arg.put("isNumeric", true); + } else { + LOGGER.warn("PostgreSQL data type argument can be primitive type only. Class '{}' is provided", + value.getClass()); + } + arg.put("argumentValue", value); + return arg; + } + + /** + * Generates default value codegen property for PostgreSQL column definition + * Ref: https://www.postgresql.org/docs/17/datatype.html + * + * @param defaultValue value + * @param postgresqlDataType PostgreSQL data type + * @return generated codegen property + */ + public HashMap toCodegenPostgresqlDataTypeDefault(String defaultValue, String postgresqlDataType) { + HashMap defaultMap = new HashMap<>(); + if (defaultValue == null || defaultValue.toUpperCase(Locale.ROOT).equals("NULL")) { + defaultMap.put("defaultValue", "NULL"); + defaultMap.put("isString", false); + defaultMap.put("isNumeric", false); + defaultMap.put("isKeyword", true); + return defaultMap; + } + + switch (postgresqlDataType.toUpperCase(Locale.ROOT)) { + case "SMALLINT": + case "INTEGER": + case "BIGINT": + // SERIAL DEFAULT VALUE is a special case. In the definition of an integer + // column, it is an alias for NOT NULL AUTO_INCREMENT UNIQUE + if (defaultValue.equals("SERIAL DEFAULT VALUE")) { + defaultMap.put("defaultValue", defaultValue); + defaultMap.put("isString", false); + defaultMap.put("isNumeric", false); + defaultMap.put("isKeyword", true); + + } else { + defaultMap.put("defaultValue", defaultValue); + defaultMap.put("isString", false); + defaultMap.put("isNumeric", true); + defaultMap.put("isKeyword", false); + + } + return defaultMap; + case "TIMESTAMP": + case "DATE": + // The exception is that, for TIMESTAMP,DATE columns, you can specify + // CURRENT_TIMESTAMP,CURRENT_DATE as the default + if (defaultValue.equals("CURRENT_TIMESTAMP")) { + defaultMap.put("defaultValue", defaultValue); + defaultMap.put("isString", false); + defaultMap.put("isNumeric", false); + defaultMap.put("isKeyword", true); + + } else if (defaultValue.equals("CURRENT_DATE")) { + defaultMap.put("defaultValue", defaultValue); + defaultMap.put("isString", false); + defaultMap.put("isNumeric", false); + defaultMap.put("isKeyword", true); + + } else { + defaultMap.put("defaultValue", defaultValue); + defaultMap.put("isString", true); + defaultMap.put("isNumeric", false); + defaultMap.put("isKeyword", false); + + } + return defaultMap; + case "BYTEA": + case "TEXT": + case "GEOMETRY": + case "JSON": + case "JSONB": + // The BLOB, TEXT, GEOMETRY, and JSON data types cannot be assigned a default + // value. + throw new RuntimeException( + "The BLOB, TEXT, GEOMETRY, and JSON data types cannot be assigned a default value"); + default: + defaultMap.put("defaultValue", defaultValue); + defaultMap.put("isString", true); + defaultMap.put("isNumeric", false); + defaultMap.put("isKeyword", false); + + return defaultMap; + } + } + + /** + * Finds best fitted PostgreSQL data type for integer variable based on minimum + * and maximum properties + * + * @param minimum (optional) codegen property + * @param maximum (optional) codegen property + * @param unsigned (optional) whether variable is unsigned or not + * @return PostgreSQL integer data type + */ + public String getPostgresqlMatchedIntegerDataType(Long minimum, Long maximum, Boolean unsigned) { + // we can choose fit postgresql data type + // ref: https://www.postgresql.org/docs/17/datatype-numeric.html + long min = (minimum != null) ? minimum : -2147483648L; + long max = (maximum != null) ? maximum : 2147483647L; + long actualMin = Math.min(min, max); // sometimes min and max values can be mixed up + long actualMax = Math.max(min, max); // sometimes only minimum specified and it can be pretty high + if (minimum != null && maximum != null && minimum > maximum) { + LOGGER.warn("Codegen property 'minimum' cannot be greater than 'maximum'"); + } + if (actualMin >= -32768 && actualMax <= 32767) { + return "SMALLINT"; + } else if (actualMin >= -2147483648 && actualMax <= 2147483647) { + return "INTEGER"; + } else if (actualMin < -2147483648 || actualMax > 2147483647) { + return "BIGINT"; + } + return "INTEGER"; + } + + /** + * Finds best fitted PostgreSQL data type for string variable based on minLength + * and maxLength properties + * + * @param minLength (optional) codegen property + * @param maxLength (optional) codegen property + * @return PostgreSQL string data type + */ + public String getPostgresqlMatchedStringDataType(Integer minLength, Integer maxLength) { + // we can choose fit postgresql data type + // ref: https://www.postgresql.org/docs/17/datatype-character.html + int min = (minLength != null && minLength >= 0) ? minLength : 0; + int max = (maxLength != null && maxLength >= 0) ? maxLength : 65536; + Integer actualMin = Math.min(min, max); // sometimes minLength and maxLength values can be mixed up + Integer actualMax = Math.max(min, max); // sometimes only minLength specified and it can be pretty high + if (minLength != null && maxLength != null && minLength > maxLength) { + LOGGER.warn("Codegen property 'minLength' cannot be greater than 'maxLength'"); + } + if (actualMax <= 65535) { + return "VARCHAR"; + } + return "TEXT"; + } + + /** + * Checks whether string is one of PostgreSQL Data Types + * Ref: https://dev.postgresql.com/doc/refman/8.0/en/data-type-overview.html + * + * @param dataType which needs to check + * @return true if value is correct PostgreSQL data type, otherwise false + */ + public Boolean isPostgresqlDataType(String dataType) { + return (postgresqlNumericTypes.contains(dataType.toUpperCase(Locale.ROOT)) || + postgresqlDateAndTimeTypes.contains(dataType.toUpperCase(Locale.ROOT)) || + postgresqlStringTypes.contains(dataType.toUpperCase(Locale.ROOT)) || + postgresqlSpatialTypes.contains(dataType.toUpperCase(Locale.ROOT))) || + dataType.toUpperCase(Locale.ROOT).equals("JSON") || + dataType.toUpperCase(Locale.ROOT).equals("JSONB"); + } + + /** + * Converts name to valid PostgreSQL database name + * + * @param name source name + * @return database name + */ + public String toDatabaseName(String name) { + String identifier = toPostgresqlIdentifier(name, databaseNamePrefix, databaseNameSuffix); + if (identifier.length() > IDENTIFIER_MAX_LENGTH) { + LOGGER.warn("Database name cannot exceed {} chars. Name '{}' will be truncated", IDENTIFIER_MAX_LENGTH, + name); + identifier = identifier.substring(0, IDENTIFIER_MAX_LENGTH); + } + return identifier; + } + + /** + * Converts name to valid PostgreSQL column name + * + * @param name source name + * @return table name + */ + public String toTableName(String name) { + String identifier = toPostgresqlIdentifier(name, tableNamePrefix, tableNameSuffix); + if (identifierNamingConvention.equals("snake_case")) { + identifier = underscore(identifier); + } + if (identifier.length() > IDENTIFIER_MAX_LENGTH) { + LOGGER.warn("Table name cannot exceed {} chars. Name '{}' will be truncated", IDENTIFIER_MAX_LENGTH, name); + identifier = identifier.substring(0, IDENTIFIER_MAX_LENGTH); + } + return identifier; + } + + /** + * Converts name to valid PostgreSQL column name + * + * @param name source name + * @return column name + */ + public String toColumnName(String name) { + String identifier = toPostgresqlIdentifier(name, columnNamePrefix, columnNameSuffix); + if (identifierNamingConvention.equals("snake_case")) { + identifier = underscore(identifier); + } + if (identifier.length() > IDENTIFIER_MAX_LENGTH) { + LOGGER.warn("Column name cannot exceed {} chars. Name '{}' will be truncated", IDENTIFIER_MAX_LENGTH, name); + identifier = identifier.substring(0, IDENTIFIER_MAX_LENGTH); + } + return identifier; + } + + /** + * Converts name to valid PostgreSQL identifier which can be used as database, + * table, column name + * + * @param name source name + * @param prefix when escaped name is digits only, prefix will be prepended + * @param suffix when escaped name is digits only, suffix will be appended + * @return identifier name + */ + public String toPostgresqlIdentifier(String name, String prefix, String suffix) { + String escapedName = escapePostgresqlQuotedIdentifier(name); + // Database, table, and column names cannot end with space characters. + if (escapedName.matches(".*\\s$")) { + LOGGER.warn("Database, table, and column names cannot end with space characters. Check '{}' name", name); + escapedName = escapedName.replaceAll("\\s+$", ""); + } + + // Identifiers may begin with a digit but unless quoted may not consist solely + // of digits. + if (escapedName.matches("^\\d+$")) { + LOGGER.warn("Database, table, and column names cannot consist solely of digits. Check '{}' name", name); + escapedName = prefix + escapedName + suffix; + } + + // identifier name cannot be empty + if (escapedName.isEmpty()) { + throw new RuntimeException("Empty database/table/column name for property '" + name + "' not allowed"); + } + return escapedName; + } + + /** + * Escapes PostgreSQL identifier to use it in SQL statements without backticks, + * eg. SELECT identifier FROM + * + * @param identifier source identifier + * @return escaped identifier + */ + public String escapePostgresqlUnquotedIdentifier(String identifier) { + // ASCII: [0-9,a-z,A-Z$_] (basic Latin letters, digits 0-9, dollar, underscore) + // Extended: U+0080 .. U+FFFF + Pattern regexp = Pattern.compile("[^0-9a-zA-z$_\\u0080-\\uFFFF]"); + Matcher matcher = regexp.matcher(identifier); + if (matcher.find()) { + LOGGER.warn("Identifier '{}' contains unsafe characters out of [0-9,a-z,A-Z$_] and U+0080..U+FFFF range", + identifier); + identifier = identifier.replaceAll("[^0-9a-zA-z$_\\u0080-\\uFFFF]", ""); + } + + // ASCII NUL (U+0000) and supplementary characters (U+10000 and higher) are not + // permitted in quoted or unquoted identifiers. + // Don't know how to match these characters, hope that first regexp already + // strip them + // Pattern regexp2 = Pattern.compile("[\0\uD800\uDC00-\uDBFF\uDFFF]"); + return identifier; + } + + /** + * Escapes PostgreSQL identifier to use it in SQL statements with backticks, eg. + * SELECT `identifier` FROM + * + * @param identifier source identifier + * @return escaped identifier + */ + public String escapePostgresqlQuotedIdentifier(String identifier) { + // ASCII: U+0001 .. U+007F Extended: U+0080 .. U+FFFF + Pattern regexp = Pattern.compile("[^\\u0001-\\u007F\\u0080-\\uFFFF]"); + Matcher matcher = regexp.matcher(identifier); + if (matcher.find()) { + LOGGER.warn("Identifier '{}' contains unsafe characters out of U+0001..U+007F and U+0080..U+FFFF range", + identifier); + identifier = identifier.replaceAll("[^\\u0001-\\u007F\\u0080-\\uFFFF]", ""); + } + + // ASCII NUL (U+0000) and supplementary characters (U+10000 and higher) are not + // permitted in quoted or unquoted identifiers. + // Don't know how to match these characters, hope that first regexp already + // strip them + // Pattern regexp2 = Pattern.compile("[\0\uD800\uDC00-\uDBFF\uDFFF]"); + return identifier; + } + + @Override + public String escapeReservedWord(String name) { + // *** For PostgreSQL: + // *** If table name or column name is a reserved word, + // *** it could be still used in double quotes + // *** (this is done in template by adding attributes 'tblNameQuoted' and + // 'colNameQuoted' when necessary) + + // LOGGER.warn( + // "'{}' is PostgreSQL reserved word. Do not use that word or properly escape it + // with backticks in mustache template", + // name); + return name; + } + + @Override + public String escapeQuotationMark(String input) { + // remove ' to avoid code injection + return input.replace("'", ""); + } + + @Override + public String escapeUnsafeCharacters(String input) { + return input.replace("*/", "*_/").replace("/*", "/_*"); + } + + /** + * Sets default database name for all PostgreSQL queries + * Provided value will be escaped when necessary + * + * @param databaseName source name + */ + public void setDefaultDatabaseName(String databaseName) { + String escapedName = toDatabaseName(databaseName); + if (!escapedName.equals(databaseName)) { + LOGGER.error( + "Invalid database name. '{}' cannot be used as PostgreSQL identifier. Escaped value '{}' will be used instead.", + databaseName, escapedName); + } + this.defaultDatabaseName = escapedName; + } + + /** + * Sets identifier naming convention for table names and column names. + * This is not related to database name which is defined by defaultDatabaseName + * option. + * + * @param naming identifier naming convention (snake_case|original) + */ + public void setIdentifierNamingConvention(String naming) { + switch (naming) { + case "snake_case": + case "original": + this.identifierNamingConvention = naming; + break; + default: + LOGGER.warn("\"{}\" is invalid \"identifierNamingConvention\" argument. Current \"{}\" used instead.", + naming, this.identifierNamingConvention); + } + } + + /** + * Slightly modified version of AbstractPhpCodegen.toSrcPath method. + * + * @param packageName package name + * + * @return path + */ + public String toSrcPath(String packageName) { + // Trim prefix file separators from package path + String packagePath = StringUtils.removeStart( + // Replace period, backslash, forward slash with file separator in package name + packageName.replaceAll("[\\.\\\\/]", Matcher.quoteReplacement("/")), + File.separator); + + // Trim trailing file separators from the overall path + return StringUtils.removeEnd(packagePath, File.separator); + } + + @Override + public GeneratorLanguage generatorLanguage() { + return GeneratorLanguage.POSTGRESQL; + } +} diff --git a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig index 636270f0a5ca..e5f7b479fb43 100644 --- a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig +++ b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig @@ -86,6 +86,7 @@ org.openapitools.codegen.languages.LuaClientCodegen org.openapitools.codegen.languages.MarkdownDocumentationCodegen org.openapitools.codegen.languages.JavaMicroprofileServerCodegen org.openapitools.codegen.languages.MysqlSchemaCodegen +org.openapitools.codegen.languages.PostgresqlSchemaCodegen org.openapitools.codegen.languages.N4jsClientCodegen org.openapitools.codegen.languages.NimClientCodegen org.openapitools.codegen.languages.NodeJSExpressServerCodegen diff --git a/modules/openapi-generator/src/main/resources/postgresql-schema/README.mustache b/modules/openapi-generator/src/main/resources/postgresql-schema/README.mustache new file mode 100644 index 000000000000..744281057fee --- /dev/null +++ b/modules/openapi-generator/src/main/resources/postgresql-schema/README.mustache @@ -0,0 +1,48 @@ +# PostgreSQL Schema Codegen + +Main goal of this generator is to provide PostgreSQL database DDL script that drops and then creates database objects for the given OpenAPI application + +[PostgreSQL documentation](https://dev.postgresql.com/doc/) + +## Requirements +- PostgreSQL Server v9.4 or newer + +## OpenAPI Data Type to PostgreSQL data type mapping + +| OpenAPI data type | OpenAPI data format | Dependent properties | PostgreSQL data types | Default PostgreSQL data type | +| --- | --- | --- | --- | --- | +| `integer` | `int32` | `minimum` / `maximum` / `minimumExclusive` / `maximumExclusive` | `SMALLINT` / `INT` / `BIGINT` | `INT` | +| `integer` | `int64` | `minimum` / `maximum` / `minimumExclusive` / `maximumExclusive` | `SMALLINT` / `INT` / `BIGINT` | `BIGINT` | +| `boolean` | | | `BOOLEAN` | `BOOLEAN` | +| `number` | `float` | | `DECIMAL` | `DECIMAL` | +| `number` | `double` | | `DECIMAL` | `DECIMAL` | +| `string` | | `minLength` / `maxLength` | `VARCHAR` / `TEXT` | `TEXT` | +| `string` | `byte` | | `BYTEA` | `BYTEA` | +| `string` | `binary` | | `BYTEA` | `BYTEA` | +| `file` | | | `BYTEA` | `BYTEA` | +| `string` | `date` | | `DATE` | `DATE` | +| `string` | `date-time` | | `TIMESTAMP` | `TIMESTAMP` | +| `string` | `enum` | | `ENUM`
(via separate ENUM data type) | `ENUM`
(via separate ENUM data type) | +| `array` | | | `JSON` / `JSONB` / `TEXT` | `JSON` | +| `object` | | | `JSON` / `JSONB` / `TEXT` | `JSON` | +| `\Model\User` (referenced definition) | | | `TEXT` | `TEXT` | + +## How to use + +Produced files: + +1. `postgresql_schema.sql` that contains: + + - `DROP ...` SQL statements for dropping every table and data type generated by this script; + + - `CREATE ...` SQL statements for creating every table and data types for them (for `ENUM` types). + + *Note: For safety reasons `DROP ...` SQL statements are commented out by default. Uncomment them before use.* + + *Note: `ENUM` data types are implemented by creating separate data types first using command `CREATE TYPE` (one data type for each `ENUM` column). Then created data type is used as data type for table column.* + +2. `postgresql_schema_oauth2.sql` that contains table creation commands for Oauth2-related tables. + +3. [Model folder]({{modelSrcPath}}) contains files with sample SQL queries for each table. Copy-paste them, edit and use. + +*Note: Important! Some of SQLs(`INSERT`/`UPDATE`) contains {{#namedParametersEnabled}}named parameters eg. :namedParam{{/namedParametersEnabled}}{{^namedParametersEnabled}}question marks(`?`) which are parameter placeholders{{/namedParametersEnabled}}. You need to bind values to these params to execute query.* diff --git a/modules/openapi-generator/src/main/resources/postgresql-schema/postgresql_schema.mustache b/modules/openapi-generator/src/main/resources/postgresql-schema/postgresql_schema.mustache new file mode 100644 index 000000000000..1ed02a279f8a --- /dev/null +++ b/modules/openapi-generator/src/main/resources/postgresql-schema/postgresql_schema.mustache @@ -0,0 +1,66 @@ +-- +-- Schema objects for PostgreSQL +-- "{{appName}}" +-- Created using 'openapi-generator' ('postgresql-schema' generator) +-- (https://openapi-generator.tech/docs/generators/postgresql-schema) +-- + +-- +-- DROP OBJECTS +-- (remove comment prefix to start using DROP commands) +-- +-- TABLES +-- +{{#models}}{{#model}}{{#hasVars}}{{^isArray}}{{#vendorExtensions}}{{#x-postgresql-schema}}{{#tableDefinition}}-- DROP TABLE IF EXISTS {{#defaultDatabaseName}}{{{.}}}.{{/defaultDatabaseName}}{{#tblNameQuoted}}"{{/tblNameQuoted}}{{tblName}}{{#tblNameQuoted}}"{{/tblNameQuoted}}; +{{/tableDefinition}}{{/x-postgresql-schema}}{{/vendorExtensions}}{{/isArray}}{{/hasVars}}{{/model}}{{/models}} +-- +-- TYPES +-- +{{#models}}{{#model}}{{#hasVars}}{{^isArray}}{{#vars}}{{#vendorExtensions}}{{#x-postgresql-schema}}{{#typeDefinition}}-- DROP TYPE IF EXISTS {{typeName}}; +{{/typeDefinition}}{{/x-postgresql-schema}}{{/vendorExtensions}}{{/vars}}{{/isArray}}{{/hasVars}}{{/model}}{{/models}} + +-- +-- CREATE OBJECTS +-- +-- TYPES +-- +{{#models}}{{#model}}{{#hasVars}}{{^isArray}}{{#vars}}{{#vendorExtensions}}{{#x-postgresql-schema}}{{#typeDefinition}}CREATE TYPE {{typeName}} AS ENUM{{#typeArguments}}{{#-first}}({{/-first}}{{#isString}}'{{/isString}}{{argumentValue}}{{#isString}}'{{/isString}}{{^-last}}, {{/-last}}{{#-last}}); +{{/-last}}{{/typeArguments}}{{/typeDefinition}}{{/x-postgresql-schema}}{{/vendorExtensions}}{{/vars}}{{/isArray}}{{/hasVars}}{{/model}}{{/models}} +-- +-- TABLES +-- +{{#models}}{{#model}}{{#hasVars}}{{^isArray}}-- +-- Table {{#vendorExtensions}}{{#x-postgresql-schema}}{{#tableDefinition}}'{{tblName}}'{{/tableDefinition}}{{/x-postgresql-schema}}{{/vendorExtensions}} generated from model '{{classVarName}}' +{{#description}} +-- {{.}} +{{/description}} +-- +{{#vendorExtensions}} +{{#x-postgresql-schema}} +{{#tableDefinition}} +CREATE TABLE IF NOT EXISTS {{#defaultDatabaseName}}{{{.}}}.{{/defaultDatabaseName}}{{#tblNameQuoted}}"{{/tblNameQuoted}}{{tblName}}{{#tblNameQuoted}}"{{/tblNameQuoted}} ( +{{/tableDefinition}} +{{/x-postgresql-schema}} +{{/vendorExtensions}} + {{#vars}} + {{#vendorExtensions}} + {{#x-postgresql-schema}} + {{#columnDefinition}} + {{#colNameQuoted}}"{{/colNameQuoted}}{{colName}}{{#colNameQuoted}}"{{/colNameQuoted}} {{colDataType}}{{#colDataTypeArguments}}{{#-first}}({{/-first}}{{#isString}}'{{/isString}}{{argumentValue}}{{#isString}}'{{/isString}}{{^-last}}, {{/-last}}{{#-last}}){{/-last}}{{/colDataTypeArguments}}{{#colNotNull}} NOT NULL{{/colNotNull}}{{#colDefault}} DEFAULT {{#isString}}'{{defaultValue}}'{{/isString}}{{^isString}}{{defaultValue}}{{/isString}}{{/colDefault}}{{^-last}},{{/-last}} + {{/columnDefinition}} + {{/x-postgresql-schema}} + {{/vendorExtensions}} + {{/vars}} +{{#vendorExtensions}} +{{#x-postgresql-schema}} +{{#tableDefinition}} +); +{{/tableDefinition}} +{{/x-postgresql-schema}} +{{/vendorExtensions}} +{{#vendorExtensions}} +{{#x-postgresql-schema}}{{#tableDefinition}}{{#tblComment}}COMMENT ON TABLE {{#tblNameQuoted}}"{{/tblNameQuoted}}{{tblName}}{{#tblNameQuoted}}"{{/tblNameQuoted}} IS '{{.}}'{{/tblComment}}; +{{/tableDefinition}}{{/x-postgresql-schema}}{{/vendorExtensions}}{{#vars}}{{#vendorExtensions}}{{#x-postgresql-schema}}{{#columnDefinition}}{{#colComment}}COMMENT ON COLUMN {{#tblNameQuoted}}"{{/tblNameQuoted}}{{tblName}}{{#tblNameQuoted}}"{{/tblNameQuoted}}.{{#colNameQuoted}}"{{/colNameQuoted}}{{colName}}{{#colNameQuoted}}"{{/colNameQuoted}} IS '{{.}}'; +{{/colComment}}{{/columnDefinition}}{{/x-postgresql-schema}}{{/vendorExtensions}}{{/vars}}{{#vendorExtensions}} +{{/vendorExtensions}} +{{/isArray}}{{/hasVars}}{{/model}}{{/models}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/postgresql-schema/postgresql_schema_oauth2.mustache b/modules/openapi-generator/src/main/resources/postgresql-schema/postgresql_schema_oauth2.mustache new file mode 100644 index 000000000000..07dfdc1d441e --- /dev/null +++ b/modules/openapi-generator/src/main/resources/postgresql-schema/postgresql_schema_oauth2.mustache @@ -0,0 +1,121 @@ +{{#hasOAuthMethods}} +-- +-- OAuth2 framework tables for PostgreSQL +-- Created using 'openapi-generator' ('postgresql-schema' generator) +-- (https://openapi-generator.tech/docs/generators/postgresql-schema) +-- + +-- +-- DROP TABLES +-- (remove comment prefix to start using DROP commands) +-- +-- DROP TABLE IF EXISTS oauth_clients; +-- DROP TABLE IF EXISTS oauth_access_tokens; +-- DROP TABLE IF EXISTS oauth_authorization_codes; +-- DROP TABLE IF EXISTS oauth_refresh_tokens; +-- DROP TABLE IF EXISTS oauth_users; +-- DROP TABLE IF EXISTS oauth_scopes; +-- DROP TABLE IF EXISTS oauth_jwt; +-- DROP TABLE IF EXISTS oauth_jti; +-- DROP TABLE IF EXISTS oauth_public_keys; + + +-- +-- Table oauth_clients +-- +CREATE TABLE IF NOT EXISTS oauth_clients ( + client_id VARCHAR(80) NOT NULL PRIMARY KEY, + client_secret VARCHAR(80) DEFAULT NULL, + redirect_uri VARCHAR(2000) DEFAULT NULL, + grant_types VARCHAR(80) DEFAULT NULL, + scope VARCHAR(4000) DEFAULT NULL, + user_id VARCHAR(80) DEFAULT NULL +); + +-- +-- Table oauth_access_tokens +-- +CREATE TABLE IF NOT EXISTS oauth_access_tokens ( + access_token VARCHAR(40) NOT NULL PRIMARY KEY, + client_id VARCHAR(80) DEFAULT NULL, + user_id VARCHAR(80) DEFAULT NULL, + expires TIMESTAMP NOT NULL, + scope VARCHAR(4000) DEFAULT NULL +); + +-- +-- Table oauth_authorization_codes +-- +CREATE TABLE IF NOT EXISTS oauth_authorization_codes ( + authorization_code VARCHAR(40) NOT NULL PRIMARY KEY, + client_id VARCHAR(80) DEFAULT NULL, + user_id VARCHAR(80) DEFAULT NULL, + redirect_uri VARCHAR(2000) NOT NULL, + expires TIMESTAMP NOT NULL, + scope VARCHAR(4000) DEFAULT NULL, + id_token VARCHAR(1000) DEFAULT NULL +); + +-- +-- Table oauth_refresh_tokens +-- +CREATE TABLE IF NOT EXISTS oauth_refresh_tokens ( + refresh_token VARCHAR(40) NOT NULL PRIMARY KEY, + client_id VARCHAR(80), + user_id VARCHAR(80), + expires TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + scope VARCHAR(4000) +); + +-- +-- Table oauth_users +-- +CREATE TABLE IF NOT EXISTS oauth_users ( + username VARCHAR(80) DEFAULT NULL, + password VARCHAR(255) DEFAULT NULL, + first_name VARCHAR(80) DEFAULT NULL, + last_name VARCHAR(80) DEFAULT NULL, + email VARCHAR(2000) DEFAULT NULL, + email_verified SMALLINT DEFAULT NULL, + scope VARCHAR(4000) DEFAULT NULL +); + +-- +-- Table oauth_scopes +-- +CREATE TABLE IF NOT EXISTS oauth_scopes ( + scope VARCHAR(80) NOT NULL PRIMARY KEY, + is_default SMALLINT DEFAULT NULL +); + +-- +-- Table oauth_jwt +-- +CREATE TABLE IF NOT EXISTS oauth_jwt ( + client_id VARCHAR(80) NOT NULL, + subject VARCHAR(80) DEFAULT NULL, + public_key VARCHAR(2000) NOT NULL +); + +-- +-- Table oauth_jti +-- +CREATE TABLE IF NOT EXISTS oauth_jti ( + issuer VARCHAR(80) NOT NULL, + subject VARCHAR(80) DEFAULT NULL, + audience VARCHAR(80) DEFAULT NULL, + expires TIMESTAMP NOT NULL, + jti VARCHAR(2000) NOT NULL +); + +-- +-- Table oauth_public_keys +-- +CREATE TABLE IF NOT EXISTS oauth_public_keys ( + client_id VARCHAR(80) DEFAULT NULL, + public_key VARCHAR(2000) DEFAULT NULL, + private_key VARCHAR(2000) DEFAULT NULL, + encryption_algorithm VARCHAR(100) DEFAULT 'RS256' +); +{{/hasOAuthMethods}} + diff --git a/modules/openapi-generator/src/main/resources/postgresql-schema/query_examples.mustache b/modules/openapi-generator/src/main/resources/postgresql-schema/query_examples.mustache new file mode 100644 index 000000000000..ee9c95e35474 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/postgresql-schema/query_examples.mustache @@ -0,0 +1,28 @@ +-- +-- "{{appName}}" +-- Prepared SQL queries for {{#models}}{{#model}}'{{{name}}}'{{/model}}{{/models}} definition. +-- Created using 'openapi-generator' ('postgresql-schema' generator) +-- (https://openapi-generator.tech/docs/generators/postgresql-schema) +-- + +{{#models}}{{#model}} +-- +-- SELECT template for table {{#vendorExtensions}}{{#x-postgresql-schema}}{{#tableDefinition}}'{{tblName}}'{{/tableDefinition}}{{/x-postgresql-schema}}{{/vendorExtensions}} +-- +SELECT {{#vars}}{{#vendorExtensions}}{{#x-postgresql-schema}}{{#columnDefinition}}{{#colNameQuoted}}"{{/colNameQuoted}}{{colName}}{{#colNameQuoted}}"{{/colNameQuoted}}{{^-last}}, {{/-last}}{{/columnDefinition}}{{/x-postgresql-schema}}{{/vendorExtensions}}{{/vars}} FROM {{#vendorExtensions}}{{#x-postgresql-schema}}{{#tableDefinition}}{{#defaultDatabaseName}}{{{.}}}.{{/defaultDatabaseName}}{{#tblNameQuoted}}"{{/tblNameQuoted}}{{tblName}}{{#tblNameQuoted}}"{{/tblNameQuoted}}{{/tableDefinition}}{{/x-postgresql-schema}}{{/vendorExtensions}} WHERE 1=1; + +-- +-- INSERT template for table {{#vendorExtensions}}{{#x-postgresql-schema}}{{#tableDefinition}}'{{tblName}}'{{/tableDefinition}}{{/x-postgresql-schema}}{{/vendorExtensions}} +-- +INSERT INTO {{#vendorExtensions}}{{#x-postgresql-schema}}{{#tableDefinition}}{{#defaultDatabaseName}}{{{.}}}.{{/defaultDatabaseName}}{{#tblNameQuoted}}"{{/tblNameQuoted}}{{tblName}}{{#tblNameQuoted}}"{{/tblNameQuoted}}{{/tableDefinition}}{{/x-postgresql-schema}}{{/vendorExtensions}} ({{#vars}}{{#vendorExtensions}}{{#x-postgresql-schema}}{{#columnDefinition}}{{#colNameQuoted}}"{{/colNameQuoted}}{{colName}}{{#colNameQuoted}}"{{/colNameQuoted}}{{^-last}}, {{/-last}}{{/columnDefinition}}{{/x-postgresql-schema}}{{/vendorExtensions}}{{/vars}}) VALUES ({{#vars}}{{#vendorExtensions}}{{#x-postgresql-schema}}{{#columnDefinition}}{{#namedParametersEnabled}}:{{colName}}{{/namedParametersEnabled}}{{^namedParametersEnabled}}?{{/namedParametersEnabled}}{{^-last}}, {{/-last}}{{/columnDefinition}}{{/x-postgresql-schema}}{{/vendorExtensions}}{{/vars}}); + +-- +-- UPDATE template for table {{#vendorExtensions}}{{#x-postgresql-schema}}{{#tableDefinition}}'{{tblName}}'{{/tableDefinition}}{{/x-postgresql-schema}}{{/vendorExtensions}} +-- +UPDATE {{#vendorExtensions}}{{#x-postgresql-schema}}{{#tableDefinition}}{{#defaultDatabaseName}}`{{{.}}}`.{{/defaultDatabaseName}}{{#tblNameQuoted}}"{{/tblNameQuoted}}{{tblName}}{{#tblNameQuoted}}"{{/tblNameQuoted}}{{/tableDefinition}}{{/x-postgresql-schema}}{{/vendorExtensions}} SET {{#vars}}{{#vendorExtensions}}{{#x-postgresql-schema}}{{#columnDefinition}}{{#colNameQuoted}}"{{/colNameQuoted}}{{colName}}{{#colNameQuoted}}"{{/colNameQuoted}} = {{#namedParametersEnabled}}:{{colName}}{{/namedParametersEnabled}}{{^namedParametersEnabled}}?{{/namedParametersEnabled}}{{^-last}}, {{/-last}}{{/columnDefinition}}{{/x-postgresql-schema}}{{/vendorExtensions}}{{/vars}} WHERE 1=2; + +-- +-- DELETE template for table {{#vendorExtensions}}{{#x-postgresql-schema}}{{#tableDefinition}}'{{tblName}}'{{/tableDefinition}}{{/x-postgresql-schema}}{{/vendorExtensions}} +-- +DELETE FROM {{#vendorExtensions}}{{#x-postgresql-schema}}{{#tableDefinition}}{{#defaultDatabaseName}}`{{{.}}}`.{{/defaultDatabaseName}}{{#tblNameQuoted}}"{{/tblNameQuoted}}{{tblName}}{{#tblNameQuoted}}"{{/tblNameQuoted}}{{/tableDefinition}}{{/x-postgresql-schema}}{{/vendorExtensions}} WHERE 1=2; +{{/model}}{{/models}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/PostgresqlSchemaOptionsProvider.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/PostgresqlSchemaOptionsProvider.java new file mode 100644 index 000000000000..d84e43f003b2 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/PostgresqlSchemaOptionsProvider.java @@ -0,0 +1,51 @@ +/* + * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.codegen.options; + +import com.google.common.collect.ImmutableMap; +import org.openapitools.codegen.languages.PostgresqlSchemaCodegen; + +import java.util.Map; + +public class PostgresqlSchemaOptionsProvider implements OptionsProvider { + public static final String DEFAULT_DATABASE_NAME_VALUE = "database_name"; + public static final String JSON_DATA_TYPE_VALUE = "json"; + public static final String IDENTIFIER_NAMING_CONVENTION_VALUE = "snake_case"; + public static final String NAMED_PARAMETERS_ENABLED_VALUE = "true"; + public static final String ID_AUTOINC_ENABLED_VALUE = "false"; + + @Override + public String getLanguage() { + return "postgresql-schema"; + } + + @Override + public Map createOptions() { + ImmutableMap.Builder builder = new ImmutableMap.Builder(); + return builder.put(PostgresqlSchemaCodegen.DEFAULT_DATABASE_NAME, DEFAULT_DATABASE_NAME_VALUE) + .put(PostgresqlSchemaCodegen.JSON_DATA_TYPE, JSON_DATA_TYPE_VALUE) + .put(PostgresqlSchemaCodegen.IDENTIFIER_NAMING_CONVENTION, IDENTIFIER_NAMING_CONVENTION_VALUE) + .put(PostgresqlSchemaCodegen.NAMED_PARAMETERS_ENABLED, NAMED_PARAMETERS_ENABLED_VALUE) + .put(PostgresqlSchemaCodegen.ID_AUTOINC_ENABLED, ID_AUTOINC_ENABLED_VALUE) + .build(); + } + + @Override + public boolean isServer() { + return false; + } +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/postgresql/PostgresqlSchemaCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/postgresql/PostgresqlSchemaCodegenTest.java new file mode 100644 index 000000000000..e97613dce294 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/postgresql/PostgresqlSchemaCodegenTest.java @@ -0,0 +1,308 @@ +/* + * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.codegen.postgresql; + +import org.openapitools.codegen.languages.PostgresqlSchemaCodegen; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Set; + +public class PostgresqlSchemaCodegenTest { + + @Test + public void testGetPostgresqlMatchedIntegerDataType() { + final PostgresqlSchemaCodegen codegen = new PostgresqlSchemaCodegen(); + Assert.assertSame(codegen.getPostgresqlMatchedIntegerDataType(null, null, null), "INTEGER"); + + Assert.assertSame(codegen.getPostgresqlMatchedIntegerDataType(-128L, 0L, false), "SMALLINT"); + Assert.assertSame(codegen.getPostgresqlMatchedIntegerDataType(0L, 255L, false), "SMALLINT"); + Assert.assertSame(codegen.getPostgresqlMatchedIntegerDataType(500L, 100L, null), "SMALLINT"); + Assert.assertSame(codegen.getPostgresqlMatchedIntegerDataType(500L, 100L, false), "SMALLINT"); + Assert.assertSame(codegen.getPostgresqlMatchedIntegerDataType(-32768L, 32767L, false), "SMALLINT"); + Assert.assertSame(codegen.getPostgresqlMatchedIntegerDataType(0L, 65535L, false), "INTEGER"); + + Assert.assertSame(codegen.getPostgresqlMatchedIntegerDataType(-8388608L, 0L, false), "INTEGER"); + Assert.assertSame(codegen.getPostgresqlMatchedIntegerDataType(0L, 16777215L, false), "INTEGER"); + Assert.assertSame(codegen.getPostgresqlMatchedIntegerDataType(Long.parseLong(String.valueOf(Integer.MIN_VALUE)), + 0L, false), "INTEGER"); + Assert.assertSame(codegen.getPostgresqlMatchedIntegerDataType(0L, + Long.parseLong(String.valueOf(Integer.MAX_VALUE)), false), "INTEGER"); + + Assert.assertSame(codegen.getPostgresqlMatchedIntegerDataType(0L, 4294967295L, false), "BIGINT"); + Assert.assertSame(codegen.getPostgresqlMatchedIntegerDataType(-2147483649L, 0L, false), "BIGINT"); + } + + @Test + public void testGetPostgresqlMatchedStringDataType() { + final PostgresqlSchemaCodegen codegen = new PostgresqlSchemaCodegen(); + Assert.assertSame(codegen.getPostgresqlMatchedStringDataType(6, 6), "VARCHAR"); + Assert.assertSame(codegen.getPostgresqlMatchedStringDataType(0, 0), "VARCHAR"); + Assert.assertSame(codegen.getPostgresqlMatchedStringDataType(255, 255), "VARCHAR"); + + Assert.assertSame(codegen.getPostgresqlMatchedStringDataType(null, 100), "VARCHAR"); + Assert.assertSame(codegen.getPostgresqlMatchedStringDataType(null, 255), "VARCHAR"); + Assert.assertSame(codegen.getPostgresqlMatchedStringDataType(50, 255), "VARCHAR"); + Assert.assertSame(codegen.getPostgresqlMatchedStringDataType(100, 20), "VARCHAR"); + + Assert.assertSame(codegen.getPostgresqlMatchedStringDataType(null, null), "TEXT"); + Assert.assertSame(codegen.getPostgresqlMatchedStringDataType(100, null), "TEXT"); + Assert.assertSame(codegen.getPostgresqlMatchedStringDataType(255, null), "TEXT"); + Assert.assertSame(codegen.getPostgresqlMatchedStringDataType(null, 256), "VARCHAR"); + + Assert.assertSame(codegen.getPostgresqlMatchedStringDataType(16777215, null), "TEXT"); + Assert.assertSame(codegen.getPostgresqlMatchedStringDataType(16777215, 100), "TEXT"); + Assert.assertSame(codegen.getPostgresqlMatchedStringDataType(null, 16777215), "TEXT"); + Assert.assertSame(codegen.getPostgresqlMatchedStringDataType(100, 16777215), "TEXT"); + + Assert.assertSame(codegen.getPostgresqlMatchedStringDataType(16777216, null), "TEXT"); + Assert.assertSame(codegen.getPostgresqlMatchedStringDataType(null, 16777216), "TEXT"); + Assert.assertSame(codegen.getPostgresqlMatchedStringDataType(16777216, 16777216), "TEXT"); + Assert.assertSame(codegen.getPostgresqlMatchedStringDataType(100, 16777216), "TEXT"); + Assert.assertSame(codegen.getPostgresqlMatchedStringDataType(100, Integer.MAX_VALUE), "TEXT"); + } + + @Test + public void testToCodegenPostgresqlDataTypeArgument() { + final PostgresqlSchemaCodegen codegen = new PostgresqlSchemaCodegen(); + String strArgument = "HelloWorld"; + HashMap strProp = codegen.toCodegenPostgresqlDataTypeArgument(strArgument); + Assert.assertTrue((Boolean) strProp.get("isString")); + Assert.assertFalse((Boolean) strProp.get("isFloat")); + Assert.assertFalse((Boolean) strProp.get("isInteger")); + Assert.assertFalse((Boolean) strProp.get("isNumeric")); + Assert.assertSame(strProp.get("argumentValue"), strArgument); + + Integer intArgument = 10; + HashMap intProp = codegen.toCodegenPostgresqlDataTypeArgument(intArgument); + Assert.assertFalse((Boolean) intProp.get("isString")); + Assert.assertFalse((Boolean) intProp.get("isFloat")); + Assert.assertTrue((Boolean) intProp.get("isInteger")); + Assert.assertTrue((Boolean) intProp.get("isNumeric")); + Assert.assertSame(intProp.get("argumentValue"), intArgument); + + Double floatArgument = 3.14; + HashMap floatProp = codegen.toCodegenPostgresqlDataTypeArgument(floatArgument); + Assert.assertFalse((Boolean) floatProp.get("isString")); + Assert.assertTrue((Boolean) floatProp.get("isFloat")); + Assert.assertFalse((Boolean) floatProp.get("isInteger")); + Assert.assertTrue((Boolean) floatProp.get("isNumeric")); + Assert.assertSame(floatProp.get("argumentValue"), floatArgument); + } + + @Test + public void testToCodegenPostgresqlDataTypeDefault() { + final PostgresqlSchemaCodegen codegen = new PostgresqlSchemaCodegen(); + HashMap defaultMap = null; + ArrayList intFixture = new ArrayList(Arrays.asList( + "SMALLINT", "INTEGER", "BIGINT")); + for (String intType : intFixture) { + defaultMap = codegen.toCodegenPostgresqlDataTypeDefault("150", intType); + Assert.assertTrue((Boolean) defaultMap.get("isNumeric")); + Assert.assertFalse((Boolean) defaultMap.get("isString")); + Assert.assertFalse((Boolean) defaultMap.get("isKeyword")); + Assert.assertSame(defaultMap.get("defaultValue"), "150"); + } + + ArrayList dateFixture = new ArrayList(Arrays.asList( + "TIMESTAMP", "DATE")); + for (String dateType : dateFixture) { + defaultMap = codegen.toCodegenPostgresqlDataTypeDefault("2018-08-12", dateType); + Assert.assertFalse((Boolean) defaultMap.get("isNumeric")); + Assert.assertTrue((Boolean) defaultMap.get("isString")); + Assert.assertFalse((Boolean) defaultMap.get("isKeyword")); + Assert.assertSame(defaultMap.get("defaultValue"), "2018-08-12"); + } + defaultMap = codegen.toCodegenPostgresqlDataTypeDefault("CURRENT_TIMESTAMP", "TIMESTAMP"); + Assert.assertFalse((Boolean) defaultMap.get("isNumeric")); + Assert.assertFalse((Boolean) defaultMap.get("isString")); + Assert.assertTrue((Boolean) defaultMap.get("isKeyword")); + Assert.assertSame(defaultMap.get("defaultValue"), "CURRENT_TIMESTAMP"); + + defaultMap = codegen.toCodegenPostgresqlDataTypeDefault("CURRENT_DATE", "DATE"); + Assert.assertFalse((Boolean) defaultMap.get("isNumeric")); + Assert.assertFalse((Boolean) defaultMap.get("isString")); + Assert.assertTrue((Boolean) defaultMap.get("isKeyword")); + Assert.assertSame(defaultMap.get("defaultValue"), "CURRENT_DATE"); + } + + @Test + public void testIsPostgresqlDataType() { + final PostgresqlSchemaCodegen codegen = new PostgresqlSchemaCodegen(); + ArrayList trueFixture = new ArrayList(Arrays.asList( + "INTEGER", "Integer", "INT", "int", "Int", "TIMESTAMP", "timestamp", "TimeStamp", "VARCHAR", "varchar", + "VarChar", "JSON", "json", "Json", "JSONB", "jsonb", "Jsonb")); + ArrayList falseFixture = new ArrayList(Arrays.asList( + "unknown", "HashMap", "HASHMAP", "hashmap")); + for (String trueValue : trueFixture) { + Assert.assertTrue(codegen.isPostgresqlDataType(trueValue), + "'" + trueValue + "' isn't PostgreSQL data type"); + } + for (String falseValue : falseFixture) { + Assert.assertFalse(codegen.isPostgresqlDataType(falseValue), + "'" + falseValue + "' is PostgreSQL data type"); + } + } + + @Test + public void testToPostgresqlIdentifier() { + final PostgresqlSchemaCodegen codegen = new PostgresqlSchemaCodegen(); + Assert.assertEquals(codegen.toPostgresqlIdentifier("table_name", "tbl_", ""), "table_name"); + Assert.assertEquals(codegen.toPostgresqlIdentifier("table_name ", "tbl_", ""), "table_name"); + Assert.assertEquals(codegen.toPostgresqlIdentifier("12345678", "tbl_", ""), "tbl_12345678"); + } + + @Test(expectedExceptions = RuntimeException.class) + public void testToPostgresqlIdentifierWithEmptyString() { + final PostgresqlSchemaCodegen codegen = new PostgresqlSchemaCodegen(); + codegen.toPostgresqlIdentifier(" ", "tbl_", ""); + } + + @Test + public void testEscapePostgresqlUnquotedIdentifier() { + final PostgresqlSchemaCodegen codegen = new PostgresqlSchemaCodegen(); + Assert.assertEquals(codegen.escapePostgresqlUnquotedIdentifier("table1Z$_"), "table1Z$_"); + Assert.assertEquals(codegen.escapePostgresqlUnquotedIdentifier("table1Z$_!#%~&?()*+-./"), "table1Z$_"); + Assert.assertEquals(codegen.escapePostgresqlUnquotedIdentifier("table1Z$_русскийтекст"), + "table1Z$_русскийтекст"); + Assert.assertEquals(codegen.escapePostgresqlQuotedIdentifier("table𐀀"), "table"); + Assert.assertEquals(codegen.escapePostgresqlQuotedIdentifier("table_name!'()�"), "table_name!'()�"); + Assert.assertEquals(codegen.escapePostgresqlQuotedIdentifier("table_name𐌅𐌌"), "table_name"); + } + + @Test + public void testEscapePostgresqlQuotedIdentifier() { + final PostgresqlSchemaCodegen codegen = new PostgresqlSchemaCodegen(); + Assert.assertEquals(codegen.escapePostgresqlQuotedIdentifier("table"), "table"); + Assert.assertEquals(codegen.escapePostgresqlQuotedIdentifier("table𐀀"), "table"); + Assert.assertEquals(codegen.escapePostgresqlQuotedIdentifier("table_name!'()�"), "table_name!'()�"); + Assert.assertEquals(codegen.escapePostgresqlQuotedIdentifier("table_name𐌅𐌌"), "table_name"); + } + + @Test + public void testIsReservedWord() { + final PostgresqlSchemaCodegen codegen = new PostgresqlSchemaCodegen(); + Set reservedWords = codegen.reservedWords(); + ArrayList trueFixture = new ArrayList(Arrays.asList( + "abort", "absent", "access", "action", "admin", "after", "alter", "always", "array", "atomic", "attach", "base64", "before", "begin", "bigint", "binary", "btrim", "cache", "called", "chain", "check", "class", "close", "cobol", "column", "commit", "count", "create", "cross", "cursor", "cycle", "define", "degree", "delete", "depth", "deref", "detach", "domain", "double", "empty", "enable", "equals", "error", "escape", "event", "every", "except", "exists", "false", "family", "fetch", "filter", "final", "finish", "first", "float", "floor", "force", "format", "found", "freeze", "fusion", "global", "grant", "group", "groups", "having", "header", "ignore", "ilike", "import", "indent", "index", "inline", "inner", "inout", "input", "insert", "isnull", "label", "large", "least", "length", "level", "limit", "listen", "local", "locked", "log10", "logged", "lower", "ltrim", "match", "member", "merge", "method", "minute", "module", "month", "mumps", "names", "nchar", "nclob", "nested", "notify", "nowait", "ntile", "nullif", "nulls", "number", "object", "octets", "offset", "option", "order", "others", "outer", "output", "owned", "owner", "parser", "pascal", "period", "plans", "policy", "power", "prior", "prune", "public", "quote", "quotes", "range", "reads", "rename", "reset", "result", "return", "revoke", "right", "rollup", "rtrim", "scalar", "scale", "schema", "scope", "scroll", "search", "second", "select", "server", "setof", "share", "simple", "source", "space", "stable", "start", "state", "static", "stdin", "stdout", "stored", "strict", "string", "strip", "style", "subset", "sysid", "system", "table", "tables", "target", "token", "treat", "types", "under", "union", "unique", "unlink", "unnest", "until", "update", "upper", "usage", "using", "utf16", "utf32", "vacuum" + )); + ArrayList falseFixture = new ArrayList(Arrays.asList( + "after_nine", "cpu", "delay_key_write", "form", "host", "install", "key_block_size", "max_size", "noo_one", "particle", "quarter", "relay", "first_do", "status", "until_now", "variables" + )); + for(String trueValue : trueFixture) { + Assert.assertTrue(reservedWords.contains(trueValue), "'" + trueValue + "' isn't PostgreSQL reserved word"); + } + for(String falseValue : falseFixture) { + Assert.assertFalse(reservedWords.contains(falseValue), "'" + falseValue + "' is PostgreSQL reserved word"); + } + } + + @Test + public void testSetDefaultDatabaseName() { + final PostgresqlSchemaCodegen codegen = new PostgresqlSchemaCodegen(); + codegen.setDefaultDatabaseName("valid_db_name"); + Assert.assertSame(codegen.getDefaultDatabaseName(), "valid_db_name"); + codegen.setDefaultDatabaseName("12345"); + Assert.assertNotSame(codegen.getDefaultDatabaseName(), "12345"); + } + + @Test + public void testGetDefaultDatabaseName() { + final PostgresqlSchemaCodegen codegen = new PostgresqlSchemaCodegen(); + Assert.assertSame(codegen.getDefaultDatabaseName(), ""); + } + + @Test + public void testSetJsonDataType() { + final PostgresqlSchemaCodegen codegen = new PostgresqlSchemaCodegen(); + Assert.assertSame("json", codegen.getJsonDataType()); + codegen.setJsonDataType("off"); + Assert.assertSame("off", codegen.getJsonDataType()); + codegen.setJsonDataType("json"); + Assert.assertSame("json", codegen.getJsonDataType()); + codegen.setJsonDataType("jsonb"); + Assert.assertSame("jsonb", codegen.getJsonDataType()); + } + + @Test + public void testGetJsonDataType() { + final PostgresqlSchemaCodegen codegen = new PostgresqlSchemaCodegen(); + Assert.assertSame("json", codegen.getJsonDataType()); + codegen.setJsonDataType("jsonb"); + Assert.assertSame("jsonb", codegen.getJsonDataType()); + codegen.setJsonDataType("off"); + Assert.assertSame("off", codegen.getJsonDataType()); + } + + @Test + public void testSetNamedParametersEnabled() { + final PostgresqlSchemaCodegen codegen = new PostgresqlSchemaCodegen(); + codegen.setNamedParametersEnabled(true); + Assert.assertTrue(codegen.getNamedParametersEnabled()); + codegen.setNamedParametersEnabled(false); + Assert.assertFalse(codegen.getNamedParametersEnabled()); + } + + @Test + public void testGetNamedParametersEnabled() { + final PostgresqlSchemaCodegen codegen = new PostgresqlSchemaCodegen(); + Assert.assertFalse(codegen.getNamedParametersEnabled()); + codegen.setNamedParametersEnabled(true); + Assert.assertTrue(codegen.getNamedParametersEnabled()); + } + + @Test + public void testSetIdentifierNamingConvention() { + final PostgresqlSchemaCodegen codegen = new PostgresqlSchemaCodegen(); + Assert.assertSame("snake_case", codegen.getIdentifierNamingConvention()); + codegen.setIdentifierNamingConvention("invalidValue"); + Assert.assertSame("snake_case", codegen.getIdentifierNamingConvention()); + codegen.setIdentifierNamingConvention("original"); + Assert.assertSame("original", codegen.getIdentifierNamingConvention()); + codegen.setIdentifierNamingConvention("anotherInvalid"); + Assert.assertSame("original", codegen.getIdentifierNamingConvention()); + } + + @Test + public void testGetIdentifierNamingConvention() { + final PostgresqlSchemaCodegen codegen = new PostgresqlSchemaCodegen(); + Assert.assertSame("snake_case", codegen.getIdentifierNamingConvention()); + codegen.setIdentifierNamingConvention("original"); + Assert.assertSame("original", codegen.getIdentifierNamingConvention()); + } + + @Test + public void testSetIdAutoIncEnabled() { + final PostgresqlSchemaCodegen codegen = new PostgresqlSchemaCodegen(); + codegen.setIdAutoIncEnabled(true); + Assert.assertTrue(codegen.getIdAutoIncEnabled()); + codegen.setIdAutoIncEnabled(false); + Assert.assertFalse(codegen.getIdAutoIncEnabled()); + } + + @Test + public void testGetIdAutoIncEnabled() { + final PostgresqlSchemaCodegen codegen = new PostgresqlSchemaCodegen(); + Assert.assertFalse(codegen.getIdAutoIncEnabled()); + codegen.setIdAutoIncEnabled(true); + Assert.assertTrue(codegen.getIdAutoIncEnabled()); + } + +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/postgresql/PostgresqlSchemaOptionsTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/postgresql/PostgresqlSchemaOptionsTest.java new file mode 100644 index 000000000000..6c40cfe0ed27 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/postgresql/PostgresqlSchemaOptionsTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.codegen.postgresql; + +import org.openapitools.codegen.AbstractOptionsTest; +import org.openapitools.codegen.CodegenConfig; +import org.openapitools.codegen.languages.PostgresqlSchemaCodegen; +import org.openapitools.codegen.options.PostgresqlSchemaOptionsProvider; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class PostgresqlSchemaOptionsTest extends AbstractOptionsTest { + private PostgresqlSchemaCodegen clientCodegen = mock(PostgresqlSchemaCodegen.class, mockSettings); + + public PostgresqlSchemaOptionsTest() { + super(new PostgresqlSchemaOptionsProvider()); + } + + @Override + protected CodegenConfig getCodegenConfig() { + return clientCodegen; + } + + @SuppressWarnings("unused") + @Override + protected void verifyOptions() { + verify(clientCodegen).setDefaultDatabaseName(PostgresqlSchemaOptionsProvider.DEFAULT_DATABASE_NAME_VALUE); + verify(clientCodegen).setJsonDataType(PostgresqlSchemaOptionsProvider.JSON_DATA_TYPE_VALUE); + verify(clientCodegen).setIdentifierNamingConvention(PostgresqlSchemaOptionsProvider.IDENTIFIER_NAMING_CONVENTION_VALUE); + verify(clientCodegen).setNamedParametersEnabled(Boolean.valueOf(PostgresqlSchemaOptionsProvider.NAMED_PARAMETERS_ENABLED_VALUE)); + verify(clientCodegen).setIdAutoIncEnabled(Boolean.valueOf(PostgresqlSchemaOptionsProvider.ID_AUTOINC_ENABLED_VALUE)); + } +} diff --git a/samples/schema/petstore/postgresql/.openapi-generator-ignore b/samples/schema/petstore/postgresql/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/schema/petstore/postgresql/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/samples/schema/petstore/postgresql/.openapi-generator/FILES b/samples/schema/petstore/postgresql/.openapi-generator/FILES new file mode 100644 index 000000000000..6e3acfba2157 --- /dev/null +++ b/samples/schema/petstore/postgresql/.openapi-generator/FILES @@ -0,0 +1,9 @@ +Model/ApiResponse.sql +Model/Category.sql +Model/Order.sql +Model/Pet.sql +Model/Tag.sql +Model/User.sql +README.md +postgresql_schema.sql +postgresql_schema_oauth2.sql diff --git a/samples/schema/petstore/postgresql/.openapi-generator/VERSION b/samples/schema/petstore/postgresql/.openapi-generator/VERSION new file mode 100644 index 000000000000..884119126398 --- /dev/null +++ b/samples/schema/petstore/postgresql/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.11.0-SNAPSHOT diff --git a/samples/schema/petstore/postgresql/Model/ApiResponse.sql b/samples/schema/petstore/postgresql/Model/ApiResponse.sql new file mode 100644 index 000000000000..e7192e3aab8c --- /dev/null +++ b/samples/schema/petstore/postgresql/Model/ApiResponse.sql @@ -0,0 +1,28 @@ +-- +-- "OpenAPI Petstore" +-- Prepared SQL queries for 'ApiResponse' definition. +-- Created using 'openapi-generator' ('postgresql-schema' generator) +-- (https://openapi-generator.tech/docs/generators/postgresql-schema) +-- + + +-- +-- SELECT template for table 'api_response' +-- +SELECT code, "type", message FROM api_response WHERE 1=1; + +-- +-- INSERT template for table 'api_response' +-- +INSERT INTO api_response (code, "type", message) VALUES (?, ?, ?); + +-- +-- UPDATE template for table 'api_response' +-- +UPDATE api_response SET code = ?, "type" = ?, message = ? WHERE 1=2; + +-- +-- DELETE template for table 'api_response' +-- +DELETE FROM api_response WHERE 1=2; + diff --git a/samples/schema/petstore/postgresql/Model/Category.sql b/samples/schema/petstore/postgresql/Model/Category.sql new file mode 100644 index 000000000000..1675ef7da3fa --- /dev/null +++ b/samples/schema/petstore/postgresql/Model/Category.sql @@ -0,0 +1,28 @@ +-- +-- "OpenAPI Petstore" +-- Prepared SQL queries for 'Category' definition. +-- Created using 'openapi-generator' ('postgresql-schema' generator) +-- (https://openapi-generator.tech/docs/generators/postgresql-schema) +-- + + +-- +-- SELECT template for table 'category' +-- +SELECT "id", "name" FROM category WHERE 1=1; + +-- +-- INSERT template for table 'category' +-- +INSERT INTO category ("id", "name") VALUES (?, ?); + +-- +-- UPDATE template for table 'category' +-- +UPDATE category SET "id" = ?, "name" = ? WHERE 1=2; + +-- +-- DELETE template for table 'category' +-- +DELETE FROM category WHERE 1=2; + diff --git a/samples/schema/petstore/postgresql/Model/Order.sql b/samples/schema/petstore/postgresql/Model/Order.sql new file mode 100644 index 000000000000..4bef395bf0f7 --- /dev/null +++ b/samples/schema/petstore/postgresql/Model/Order.sql @@ -0,0 +1,28 @@ +-- +-- "OpenAPI Petstore" +-- Prepared SQL queries for 'Order' definition. +-- Created using 'openapi-generator' ('postgresql-schema' generator) +-- (https://openapi-generator.tech/docs/generators/postgresql-schema) +-- + + +-- +-- SELECT template for table 'order' +-- +SELECT "id", pet_id, quantity, ship_date, status, complete FROM "order" WHERE 1=1; + +-- +-- INSERT template for table 'order' +-- +INSERT INTO "order" ("id", pet_id, quantity, ship_date, status, complete) VALUES (?, ?, ?, ?, ?, ?); + +-- +-- UPDATE template for table 'order' +-- +UPDATE "order" SET "id" = ?, pet_id = ?, quantity = ?, ship_date = ?, status = ?, complete = ? WHERE 1=2; + +-- +-- DELETE template for table 'order' +-- +DELETE FROM "order" WHERE 1=2; + diff --git a/samples/schema/petstore/postgresql/Model/Pet.sql b/samples/schema/petstore/postgresql/Model/Pet.sql new file mode 100644 index 000000000000..e08b281bd7d8 --- /dev/null +++ b/samples/schema/petstore/postgresql/Model/Pet.sql @@ -0,0 +1,28 @@ +-- +-- "OpenAPI Petstore" +-- Prepared SQL queries for 'Pet' definition. +-- Created using 'openapi-generator' ('postgresql-schema' generator) +-- (https://openapi-generator.tech/docs/generators/postgresql-schema) +-- + + +-- +-- SELECT template for table 'pet' +-- +SELECT "id", category, "name", photo_urls, tags, status FROM pet WHERE 1=1; + +-- +-- INSERT template for table 'pet' +-- +INSERT INTO pet ("id", category, "name", photo_urls, tags, status) VALUES (?, ?, ?, ?, ?, ?); + +-- +-- UPDATE template for table 'pet' +-- +UPDATE pet SET "id" = ?, category = ?, "name" = ?, photo_urls = ?, tags = ?, status = ? WHERE 1=2; + +-- +-- DELETE template for table 'pet' +-- +DELETE FROM pet WHERE 1=2; + diff --git a/samples/schema/petstore/postgresql/Model/Tag.sql b/samples/schema/petstore/postgresql/Model/Tag.sql new file mode 100644 index 000000000000..abd78b077a77 --- /dev/null +++ b/samples/schema/petstore/postgresql/Model/Tag.sql @@ -0,0 +1,28 @@ +-- +-- "OpenAPI Petstore" +-- Prepared SQL queries for 'Tag' definition. +-- Created using 'openapi-generator' ('postgresql-schema' generator) +-- (https://openapi-generator.tech/docs/generators/postgresql-schema) +-- + + +-- +-- SELECT template for table 'tag' +-- +SELECT "id", "name" FROM tag WHERE 1=1; + +-- +-- INSERT template for table 'tag' +-- +INSERT INTO tag ("id", "name") VALUES (?, ?); + +-- +-- UPDATE template for table 'tag' +-- +UPDATE tag SET "id" = ?, "name" = ? WHERE 1=2; + +-- +-- DELETE template for table 'tag' +-- +DELETE FROM tag WHERE 1=2; + diff --git a/samples/schema/petstore/postgresql/Model/User.sql b/samples/schema/petstore/postgresql/Model/User.sql new file mode 100644 index 000000000000..2798d5315f38 --- /dev/null +++ b/samples/schema/petstore/postgresql/Model/User.sql @@ -0,0 +1,28 @@ +-- +-- "OpenAPI Petstore" +-- Prepared SQL queries for 'User' definition. +-- Created using 'openapi-generator' ('postgresql-schema' generator) +-- (https://openapi-generator.tech/docs/generators/postgresql-schema) +-- + + +-- +-- SELECT template for table 'user' +-- +SELECT "id", username, first_name, last_name, email, "password", phone, user_status FROM "user" WHERE 1=1; + +-- +-- INSERT template for table 'user' +-- +INSERT INTO "user" ("id", username, first_name, last_name, email, "password", phone, user_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?); + +-- +-- UPDATE template for table 'user' +-- +UPDATE "user" SET "id" = ?, username = ?, first_name = ?, last_name = ?, email = ?, "password" = ?, phone = ?, user_status = ? WHERE 1=2; + +-- +-- DELETE template for table 'user' +-- +DELETE FROM "user" WHERE 1=2; + diff --git a/samples/schema/petstore/postgresql/README.md b/samples/schema/petstore/postgresql/README.md new file mode 100644 index 000000000000..2532c72fba0f --- /dev/null +++ b/samples/schema/petstore/postgresql/README.md @@ -0,0 +1,48 @@ +# PostgreSQL Schema Codegen + +Main goal of this generator is to provide PostgreSQL database DDL script that drops and then creates database objects for the given OpenAPI application + +[PostgreSQL documentation](https://dev.postgresql.com/doc/) + +## Requirements +- PostgreSQL Server v9.4 or newer + +## OpenAPI Data Type to PostgreSQL data type mapping + +| OpenAPI data type | OpenAPI data format | Dependent properties | PostgreSQL data types | Default PostgreSQL data type | +| --- | --- | --- | --- | --- | +| `integer` | `int32` | `minimum` / `maximum` / `minimumExclusive` / `maximumExclusive` | `SMALLINT` / `INT` / `BIGINT` | `INT` | +| `integer` | `int64` | `minimum` / `maximum` / `minimumExclusive` / `maximumExclusive` | `SMALLINT` / `INT` / `BIGINT` | `BIGINT` | +| `boolean` | | | `BOOLEAN` | `BOOLEAN` | +| `number` | `float` | | `DECIMAL` | `DECIMAL` | +| `number` | `double` | | `DECIMAL` | `DECIMAL` | +| `string` | | `minLength` / `maxLength` | `VARCHAR` / `TEXT` | `TEXT` | +| `string` | `byte` | | `BYTEA` | `BYTEA` | +| `string` | `binary` | | `BYTEA` | `BYTEA` | +| `file` | | | `BYTEA` | `BYTEA` | +| `string` | `date` | | `DATE` | `DATE` | +| `string` | `date-time` | | `TIMESTAMP` | `TIMESTAMP` | +| `string` | `enum` | | `ENUM`
(via separate ENUM data type) | `ENUM`
(via separate ENUM data type) | +| `array` | | | `JSON` / `JSONB` / `TEXT` | `JSON` | +| `object` | | | `JSON` / `JSONB` / `TEXT` | `JSON` | +| `\Model\User` (referenced definition) | | | `TEXT` | `TEXT` | + +## How to use + +Produced files: + +1. `postgresql_schema.sql` that contains: + + - `DROP ...` SQL statements for dropping every table and data type generated by this script; + + - `CREATE ...` SQL statements for creating every table and data types for them (for `ENUM` types). + + *Note: For safety reasons `DROP ...` SQL statements are commented out by default. Uncomment them before use.* + + *Note: `ENUM` data types are implemented by creating separate data types first using command `CREATE TYPE` (one data type for each `ENUM` column). Then created data type is used as data type for table column.* + +2. `postgresql_schema_oauth2.sql` that contains table creation commands for Oauth2-related tables. + +3. [Model folder](./Model) contains files with sample SQL queries for each table. Copy-paste them, edit and use. + +*Note: Important! Some of SQLs(`INSERT`/`UPDATE`) contains question marks(`?`) which are parameter placeholders. You need to bind values to these params to execute query.* diff --git a/samples/schema/petstore/postgresql/postgresql_schema.sql b/samples/schema/petstore/postgresql/postgresql_schema.sql new file mode 100644 index 000000000000..504ce2925c17 --- /dev/null +++ b/samples/schema/petstore/postgresql/postgresql_schema.sql @@ -0,0 +1,121 @@ +-- +-- Schema objects for PostgreSQL +-- "OpenAPI Petstore" +-- Created using 'openapi-generator' ('postgresql-schema' generator) +-- (https://openapi-generator.tech/docs/generators/postgresql-schema) +-- + +-- +-- DROP OBJECTS +-- (remove comment prefix to start using DROP commands) +-- +-- TABLES +-- +-- DROP TABLE IF EXISTS api_response; +-- DROP TABLE IF EXISTS category; +-- DROP TABLE IF EXISTS "order"; +-- DROP TABLE IF EXISTS pet; +-- DROP TABLE IF EXISTS tag; +-- DROP TABLE IF EXISTS "user"; + +-- +-- TYPES +-- +-- DROP TYPE IF EXISTS order_status; +-- DROP TYPE IF EXISTS pet_status; + + +-- +-- CREATE OBJECTS +-- +-- TYPES +-- +CREATE TYPE order_status AS ENUM('placed', 'approved', 'delivered'); +CREATE TYPE pet_status AS ENUM('available', 'pending', 'sold'); + +-- +-- TABLES +-- +-- +-- Table 'api_response' generated from model 'ApiResponse' +-- Describes the result of uploading an image resource +-- +CREATE TABLE IF NOT EXISTS api_response ( + code INTEGER DEFAULT NULL, + "type" TEXT DEFAULT NULL, + message TEXT DEFAULT NULL +); +COMMENT ON TABLE api_response IS 'Describes the result of uploading an image resource. Original model name - ApiResponse.'; + +-- +-- Table 'category' generated from model 'Category' +-- A category for a pet +-- +CREATE TABLE IF NOT EXISTS category ( + "id" BIGINT DEFAULT NULL, + "name" TEXT DEFAULT NULL +); +COMMENT ON TABLE category IS 'A category for a pet. Original model name - Category.'; + +-- +-- Table 'order' generated from model 'Order' +-- An order for a pets from the pet store +-- +CREATE TABLE IF NOT EXISTS "order" ( + "id" BIGINT DEFAULT NULL, + pet_id BIGINT DEFAULT NULL, + quantity INTEGER DEFAULT NULL, + ship_date TIMESTAMP DEFAULT NULL, + status order_status DEFAULT NULL, + complete BOOLEAN DEFAULT 'false' +); +COMMENT ON TABLE "order" IS 'An order for a pets from the pet store. Original model name - Order.'; +COMMENT ON COLUMN "order".pet_id IS 'Original param name - petId.'; +COMMENT ON COLUMN "order".ship_date IS 'Original param name - shipDate.'; +COMMENT ON COLUMN "order".status IS 'Order Status'; + +-- +-- Table 'pet' generated from model 'Pet' +-- A pet for sale in the pet store +-- +CREATE TABLE IF NOT EXISTS pet ( + "id" BIGINT DEFAULT NULL, + category TEXT DEFAULT NULL, + "name" TEXT NOT NULL, + photo_urls JSON NOT NULL, + tags JSON DEFAULT NULL, + status pet_status DEFAULT NULL +); +COMMENT ON TABLE pet IS 'A pet for sale in the pet store. Original model name - Pet.'; +COMMENT ON COLUMN pet.photo_urls IS 'Original param name - photoUrls.'; +COMMENT ON COLUMN pet.status IS 'pet status in the store'; + +-- +-- Table 'tag' generated from model 'Tag' +-- A tag for a pet +-- +CREATE TABLE IF NOT EXISTS tag ( + "id" BIGINT DEFAULT NULL, + "name" TEXT DEFAULT NULL +); +COMMENT ON TABLE tag IS 'A tag for a pet. Original model name - Tag.'; + +-- +-- Table 'user' generated from model 'User' +-- A User who is purchasing from the pet store +-- +CREATE TABLE IF NOT EXISTS "user" ( + "id" BIGINT DEFAULT NULL, + username TEXT DEFAULT NULL, + first_name TEXT DEFAULT NULL, + last_name TEXT DEFAULT NULL, + email TEXT DEFAULT NULL, + "password" TEXT DEFAULT NULL, + phone TEXT DEFAULT NULL, + user_status INTEGER DEFAULT NULL +); +COMMENT ON TABLE "user" IS 'A User who is purchasing from the pet store. Original model name - User.'; +COMMENT ON COLUMN "user".first_name IS 'Original param name - firstName.'; +COMMENT ON COLUMN "user".last_name IS 'Original param name - lastName.'; +COMMENT ON COLUMN "user".user_status IS 'User Status. Original param name - userStatus.'; + diff --git a/samples/schema/petstore/postgresql/postgresql_schema_oauth2.sql b/samples/schema/petstore/postgresql/postgresql_schema_oauth2.sql new file mode 100644 index 000000000000..aa1be71280b5 --- /dev/null +++ b/samples/schema/petstore/postgresql/postgresql_schema_oauth2.sql @@ -0,0 +1,119 @@ +-- +-- OAuth2 framework tables for PostgreSQL +-- Created using 'openapi-generator' ('postgresql-schema' generator) +-- (https://openapi-generator.tech/docs/generators/postgresql-schema) +-- + +-- +-- DROP TABLES +-- (remove comment prefix to start using DROP commands) +-- +-- DROP TABLE IF EXISTS oauth_clients; +-- DROP TABLE IF EXISTS oauth_access_tokens; +-- DROP TABLE IF EXISTS oauth_authorization_codes; +-- DROP TABLE IF EXISTS oauth_refresh_tokens; +-- DROP TABLE IF EXISTS oauth_users; +-- DROP TABLE IF EXISTS oauth_scopes; +-- DROP TABLE IF EXISTS oauth_jwt; +-- DROP TABLE IF EXISTS oauth_jti; +-- DROP TABLE IF EXISTS oauth_public_keys; + + +-- +-- Table oauth_clients +-- +CREATE TABLE IF NOT EXISTS oauth_clients ( + client_id VARCHAR(80) NOT NULL PRIMARY KEY, + client_secret VARCHAR(80) DEFAULT NULL, + redirect_uri VARCHAR(2000) DEFAULT NULL, + grant_types VARCHAR(80) DEFAULT NULL, + scope VARCHAR(4000) DEFAULT NULL, + user_id VARCHAR(80) DEFAULT NULL +); + +-- +-- Table oauth_access_tokens +-- +CREATE TABLE IF NOT EXISTS oauth_access_tokens ( + access_token VARCHAR(40) NOT NULL PRIMARY KEY, + client_id VARCHAR(80) DEFAULT NULL, + user_id VARCHAR(80) DEFAULT NULL, + expires TIMESTAMP NOT NULL, + scope VARCHAR(4000) DEFAULT NULL +); + +-- +-- Table oauth_authorization_codes +-- +CREATE TABLE IF NOT EXISTS oauth_authorization_codes ( + authorization_code VARCHAR(40) NOT NULL PRIMARY KEY, + client_id VARCHAR(80) DEFAULT NULL, + user_id VARCHAR(80) DEFAULT NULL, + redirect_uri VARCHAR(2000) NOT NULL, + expires TIMESTAMP NOT NULL, + scope VARCHAR(4000) DEFAULT NULL, + id_token VARCHAR(1000) DEFAULT NULL +); + +-- +-- Table oauth_refresh_tokens +-- +CREATE TABLE IF NOT EXISTS oauth_refresh_tokens ( + refresh_token VARCHAR(40) NOT NULL PRIMARY KEY, + client_id VARCHAR(80), + user_id VARCHAR(80), + expires TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + scope VARCHAR(4000) +); + +-- +-- Table oauth_users +-- +CREATE TABLE IF NOT EXISTS oauth_users ( + username VARCHAR(80) DEFAULT NULL, + password VARCHAR(255) DEFAULT NULL, + first_name VARCHAR(80) DEFAULT NULL, + last_name VARCHAR(80) DEFAULT NULL, + email VARCHAR(2000) DEFAULT NULL, + email_verified SMALLINT DEFAULT NULL, + scope VARCHAR(4000) DEFAULT NULL +); + +-- +-- Table oauth_scopes +-- +CREATE TABLE IF NOT EXISTS oauth_scopes ( + scope VARCHAR(80) NOT NULL PRIMARY KEY, + is_default SMALLINT DEFAULT NULL +); + +-- +-- Table oauth_jwt +-- +CREATE TABLE IF NOT EXISTS oauth_jwt ( + client_id VARCHAR(80) NOT NULL, + subject VARCHAR(80) DEFAULT NULL, + public_key VARCHAR(2000) NOT NULL +); + +-- +-- Table oauth_jti +-- +CREATE TABLE IF NOT EXISTS oauth_jti ( + issuer VARCHAR(80) NOT NULL, + subject VARCHAR(80) DEFAULT NULL, + audience VARCHAR(80) DEFAULT NULL, + expires TIMESTAMP NOT NULL, + jti VARCHAR(2000) NOT NULL +); + +-- +-- Table oauth_public_keys +-- +CREATE TABLE IF NOT EXISTS oauth_public_keys ( + client_id VARCHAR(80) DEFAULT NULL, + public_key VARCHAR(2000) DEFAULT NULL, + private_key VARCHAR(2000) DEFAULT NULL, + encryption_algorithm VARCHAR(100) DEFAULT 'RS256' +); +