This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch past-M2 in repository https://gitbox.apache.org/repos/asf/cayenne.git
commit 80dea98fd93786a36e67fe7e10d1fea784e4d3f7 Author: Andrus Adamchik <[email protected]> AuthorDate: Sun Jun 14 16:09:17 2026 -0400 CAY-2962 Allow unconstrained VARCHAR --- RELEASE-NOTES.txt | 1 + .../validation/ValidationSaverDelegate.java | 2 + .../project/validation/DbAttributeValidator.java | 16 +--- .../cayenne/project/validation/Inspection.java | 29 +++++++- .../validation/ProcedureParameterValidator.java | 15 ---- .../validation/ValidationConfigSaveTest.java | 86 ++++++++++++++++++++++ .../java/org/apache/cayenne/dba/AutoAdapter.java | 8 ++ .../java/org/apache/cayenne/dba/DbAdapter.java | 9 +++ .../java/org/apache/cayenne/dba/JdbcAdapter.java | 69 ++++++++++++++--- .../org/apache/cayenne/dba/NativeColumnType.java | 23 ++++-- .../java/org/apache/cayenne/dba/h2/H2Adapter.java | 8 +- .../apache/cayenne/dba/hsqldb/HSQLDBAdapter.java | 8 +- .../org/apache/cayenne/dba/mysql/MySQLAdapter.java | 4 + .../apache/cayenne/dba/oracle/OracleAdapter.java | 3 + .../cayenne/dba/postgres/PostgresAdapter.java | 32 +++----- .../cayenne/dba/sqlserver/SQLServerAdapter.java | 4 + .../java/org/apache/cayenne/dba/JdbcAdapterIT.java | 13 ++++ .../apache/cayenne/dba/NativeColumnTypeTest.java | 17 +++++ .../apache/cayenne/dba/hsqldb/HSQLDBAdapterIT.java | 17 ++++- .../apache/cayenne/dba/mysql/MySQLAdapterIT.java | 14 ++++ .../apache/cayenne/dba/oracle/OracleAdapterIT.java | 17 +++++ .../cayenne/dba/postgres/PostgresAdapterIT.java | 25 +++++++ .../cayenne/dba/sqlserver/SQLServerAdapterIT.java | 13 ++++ .../validation/InspectionCheckBoxTree.java | 3 + 24 files changed, 355 insertions(+), 81 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index e381602f4..3d9159ed7 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -16,6 +16,7 @@ Changes/New Features: CAY-2954 Don't wrap selecting queries in transactions CAY-2956 Get rid of a dedicated adapter for Oracle 8 CAY-2957 Get rid of adapter for legacy HSQLDB <= 1.8 +CAY-2962 Allow unconstrained VARCHAR CAY-2963 Replace TypesHandler / types.xml with hardcoded map Bug Fixes: diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/extension/validation/ValidationSaverDelegate.java b/cayenne-project/src/main/java/org/apache/cayenne/project/extension/validation/ValidationSaverDelegate.java index 8b0db1ebf..0a6ed55e8 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/extension/validation/ValidationSaverDelegate.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/extension/validation/ValidationSaverDelegate.java @@ -44,6 +44,8 @@ public class ValidationSaverDelegate extends BaseSaverDelegate { ValidationConfig validationConfig = ValidationConfig.fromMetadata(metaData, dataChannelDescriptor); Set<Inspection> disabledInspections = EnumSet.allOf(Inspection.class); disabledInspections.removeAll(validationConfig.getEnabledInspections()); + // deprecated inspections are no longer performed; drop them on save instead of round-tripping them + disabledInspections.removeIf(Inspection::isDeprecated); if (disabledInspections.isEmpty()) { return null; } diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DbAttributeValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DbAttributeValidator.java index f391c951e..946c5e796 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DbAttributeValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DbAttributeValidator.java @@ -40,8 +40,7 @@ class DbAttributeValidator extends ConfigurationNodeValidator<DbAttribute> { on(node, validationResult) .performIfEnabled(Inspection.DB_ATTRIBUTE_NO_NAME, this::checkForName) .performIfEnabled(Inspection.DB_ATTRIBUTE_INVALID_NAME, this::validateName) - .performIfEnabled(Inspection.DB_ATTRIBUTE_NO_TYPE, this::checkForType) - .performIfEnabled(Inspection.DB_ATTRIBUTE_NO_LENGTH, this::checkForLength); + .performIfEnabled(Inspection.DB_ATTRIBUTE_NO_TYPE, this::checkForType); } private void checkForName(DbAttribute attribute, ValidationResult validationResult) { @@ -72,17 +71,4 @@ class DbAttributeValidator extends ConfigurationNodeValidator<DbAttribute> { addFailure(validationResult, attribute, "DbAttribute has no type"); } } - - private void checkForLength(DbAttribute attribute, ValidationResult validationResult) { - if (attribute.getMaxLength() < 0 - && (attribute.getType() == java.sql.Types.VARCHAR - || attribute.getType() == java.sql.Types.NVARCHAR - || attribute.getType() == java.sql.Types.CHAR - || attribute.getType() == java.sql.Types.NCHAR)) { - // VARCHAR and CHAR attributes must have max length - - addFailure(validationResult, attribute, "Character DbAttribute '%s' doesn't have max length", - attribute.getName()); - } - } } diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/Inspection.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/Inspection.java index 136612006..b5b531643 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/Inspection.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/Inspection.java @@ -67,7 +67,8 @@ public enum Inspection { DB_ATTRIBUTE_NO_NAME(Group.DB_ATTRIBUTE, "Empty db attribute name"), DB_ATTRIBUTE_INVALID_NAME(Group.DB_ATTRIBUTE, "Invalid db attribute name"), DB_ATTRIBUTE_NO_TYPE(Group.DB_ATTRIBUTE, "Empty db attribute type"), - DB_ATTRIBUTE_NO_LENGTH(Group.DB_ATTRIBUTE, "String db attribute has no length"), + @Deprecated(since = "5.0", forRemoval = true) + DB_ATTRIBUTE_NO_LENGTH(Group.DB_ATTRIBUTE, "String db attribute has no length", true), DB_RELATIONSHIP_NO_NAME(Group.DB_RELATIONSHIP, "Empty db relationship name"), DB_RELATIONSHIP_NAME_DUPLICATE(Group.DB_RELATIONSHIP, "Duplicate of a db relationship name"), @@ -93,7 +94,8 @@ public enum Inspection { PROCEDURE_PARAMETER_NO_NAME(Group.PROCEDURE_PARAMETER, "Empty procedure parameter name"), PROCEDURE_PARAMETER_NO_TYPE(Group.PROCEDURE_PARAMETER, "Empty procedure parameter type"), - PROCEDURE_PARAMETER_NO_LENGTH(Group.PROCEDURE_PARAMETER, "String procedure parameter has no length"), + @Deprecated(since = "5.0", forRemoval = true) + PROCEDURE_PARAMETER_NO_LENGTH(Group.PROCEDURE_PARAMETER, "String procedure parameter has no length", true), PROCEDURE_PARAMETER_NO_DIRECTION(Group.PROCEDURE_PARAMETER, "Procedure parameter has no direction"), QUERY_NO_NAME(Group.QUERY, "Empty query name"), @@ -116,15 +118,25 @@ public enum Inspection { private final Group group; private final String readableName; private final String description; + private final boolean deprecated; Inspection(Group group, String name) { - this(group, name, name); + this(group, name, name, false); + } + + Inspection(Group group, String name, boolean deprecated) { + this(group, name, name, deprecated); } Inspection(Group group, String name, String description) { + this(group, name, description, false); + } + + Inspection(Group group, String name, String description, boolean deprecated) { this.group = group; this.readableName = name; this.description = description; + this.deprecated = deprecated; } public Group group() { @@ -139,6 +151,17 @@ public enum Inspection { return description; } + /** + * Returns true if this inspection is no longer performed and is retained only so previously saved project + * configurations that reference it still load. Deprecated inspections are hidden from the UI and stripped + * when a project is saved. + * + * @since 5.0 + */ + public boolean isDeprecated() { + return deprecated; + } + @Override public String toString() { return readableName(); diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ProcedureParameterValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ProcedureParameterValidator.java index dcf754f5d..06ea68c4e 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ProcedureParameterValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ProcedureParameterValidator.java @@ -18,7 +18,6 @@ ****************************************************************/ package org.apache.cayenne.project.validation; -import java.sql.Types; import java.util.function.Supplier; import org.apache.cayenne.dba.TypesMapping; @@ -41,7 +40,6 @@ class ProcedureParameterValidator extends ConfigurationNodeValidator<ProcedurePa on(node, validationResult) .performIfEnabled(Inspection.PROCEDURE_PARAMETER_NO_NAME, this::checkForName) .performIfEnabled(Inspection.PROCEDURE_PARAMETER_NO_TYPE, this::checkForType) - .performIfEnabled(Inspection.PROCEDURE_PARAMETER_NO_LENGTH, this::checkForLength) .performIfEnabled(Inspection.PROCEDURE_PARAMETER_NO_DIRECTION, this::checkForDirection); } @@ -59,19 +57,6 @@ class ProcedureParameterValidator extends ConfigurationNodeValidator<ProcedurePa } } - private void checkForLength(ProcedureParameter parameter, ValidationResult validationResult) { - // VARCHAR and CHAR attributes must have max length - if (parameter.getMaxLength() < 0 - && (parameter.getType() == Types.VARCHAR - || parameter.getType() == Types.NVARCHAR - || parameter.getType() == Types.CHAR - || parameter.getType() == Types.NCHAR)) { - - addFailure(validationResult, parameter, "Character ProcedureParameter '%s' doesn't have max length", - parameter.getName()); - } - } - private void checkForDirection(ProcedureParameter parameter, ValidationResult validationResult) { // all attributes must have type if (parameter.getDirection() <= 0) { diff --git a/cayenne-project/src/test/java/org/apache/cayenne/project/extension/validation/ValidationConfigSaveTest.java b/cayenne-project/src/test/java/org/apache/cayenne/project/extension/validation/ValidationConfigSaveTest.java new file mode 100644 index 000000000..59778fcc6 --- /dev/null +++ b/cayenne-project/src/test/java/org/apache/cayenne/project/extension/validation/ValidationConfigSaveTest.java @@ -0,0 +1,86 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.cayenne.project.extension.validation; + +import org.apache.cayenne.configuration.ConfigurationNameMapper; +import org.apache.cayenne.configuration.ConfigurationTree; +import org.apache.cayenne.configuration.DataChannelDescriptor; +import org.apache.cayenne.configuration.DefaultConfigurationNameMapper; +import org.apache.cayenne.configuration.xml.DataChannelMetaData; +import org.apache.cayenne.configuration.xml.DefaultDataChannelMetaData; +import org.apache.cayenne.di.DIBootstrap; +import org.apache.cayenne.di.Injector; +import org.apache.cayenne.di.Module; +import org.apache.cayenne.project.FileProjectSaver; +import org.apache.cayenne.project.Project; +import org.apache.cayenne.project.extension.ProjectExtension; +import org.apache.cayenne.project.validation.Inspection; +import org.apache.cayenne.project.validation.ValidationConfig; +import org.apache.cayenne.resource.URLResource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.nio.file.Files; +import java.util.EnumSet; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ValidationConfigSaveTest { + + @TempDir + public File tempDir; + + @Test + @SuppressWarnings("removal") // intentionally references a deprecated inspection + public void deprecatedInspectionStrippedOnSave() throws Exception { + Module testModule = binder -> { + binder.bind(ConfigurationNameMapper.class).to(DefaultConfigurationNameMapper.class); + binder.bind(DataChannelMetaData.class).to(DefaultDataChannelMetaData.class); + }; + Injector injector = DIBootstrap.createInjector(testModule); + + DataChannelMetaData metaData = injector.getInstance(DataChannelMetaData.class); + ValidationExtension extension = new ValidationExtension(); + injector.injectMembers(extension); + + FileProjectSaver saver = new FileProjectSaver(List.<ProjectExtension>of(extension)); + injector.injectMembers(saver); + + DataChannelDescriptor descriptor = new DataChannelDescriptor(); + descriptor.setName("deprecation"); + + // disable one deprecated inspection and one regular inspection + EnumSet<Inspection> enabled = EnumSet.complementOf(EnumSet.of( + Inspection.DB_ATTRIBUTE_NO_LENGTH, + Inspection.DATA_NODE_NO_NAME)); + metaData.add(descriptor, new ValidationConfig(enabled)); + + Project project = new Project(new ConfigurationTree<>(descriptor)); + saver.saveAs(project, new URLResource(tempDir.toURI().toURL())); + + String xml = Files.readString(new File(tempDir, "cayenne-deprecation.xml").toPath()); + + // the deprecated inspection is dropped, the regular one is round-tripped + assertFalse(xml.contains("DB_ATTRIBUTE_NO_LENGTH"), xml); + assertTrue(xml.contains("DATA_NODE_NO_NAME"), xml); + } +} diff --git a/cayenne/src/main/java/org/apache/cayenne/dba/AutoAdapter.java b/cayenne/src/main/java/org/apache/cayenne/dba/AutoAdapter.java index b67f80022..ad2c21bd0 100644 --- a/cayenne/src/main/java/org/apache/cayenne/dba/AutoAdapter.java +++ b/cayenne/src/main/java/org/apache/cayenne/dba/AutoAdapter.java @@ -171,6 +171,14 @@ public class AutoAdapter implements DbAdapter { return getAdapter().typeSupportsScale(type); } + /** + * @since 5.0 + */ + @Override + public int defaultCharLength() { + return getAdapter().defaultCharLength(); + } + @Override public Collection<String> dropTableStatements(DbEntity table) { return getAdapter().dropTableStatements(table); diff --git a/cayenne/src/main/java/org/apache/cayenne/dba/DbAdapter.java b/cayenne/src/main/java/org/apache/cayenne/dba/DbAdapter.java index 3e7503d18..3e32f9690 100644 --- a/cayenne/src/main/java/org/apache/cayenne/dba/DbAdapter.java +++ b/cayenne/src/main/java/org/apache/cayenne/dba/DbAdapter.java @@ -132,6 +132,15 @@ public interface DbAdapter { */ boolean typeSupportsScale(int type); + /** + * Returns the length to use for an unconstrained character column (a CHAR/VARCHAR-family column with no max + * length) when this database requires a length. Adapters whose database accepts a length-free character type + * instead declare an {@link NativeColumnType#asUnconstrained() unconstrained} native type. + * + * @since 5.0 + */ + int defaultCharLength(); + /** * Returns a collection of SQL statements needed to drop a database table. * diff --git a/cayenne/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java b/cayenne/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java index c4cd15ba5..1b05fea64 100644 --- a/cayenne/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java +++ b/cayenne/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java @@ -68,6 +68,8 @@ public class JdbcAdapter implements DbAdapter { private PkGenerator pkGenerator; protected QuotingStrategy quotingStrategy; + protected int defaultCharLength = 255; + protected Map<Integer, NativeColumnType[]> nativeColumnTypes; protected ExtendedTypeMap extendedTypes; protected boolean supportsBatchUpdates; @@ -291,6 +293,14 @@ public class JdbcAdapter implements DbAdapter { || type == Types.TIME || type == Types.TIMESTAMP; } + /** + * @since 5.0 + */ + @Override + public int defaultCharLength() { + return defaultCharLength; + } + /** * @since 3.0 */ @@ -379,8 +389,49 @@ public class JdbcAdapter implements DbAdapter { sqlBuffer.append(column.isMandatory() ? " NOT NULL" : " NULL"); } + /** + * Selects the native type variant to use for a column: the auto-increment variant for a generated column, + * the {@link NativeColumnType#unconstrained() unconstrained} variant for a character column with no max length, + * otherwise the primary variant. + * + * @since 5.0 + */ + public static NativeColumnType selectNativeType(DbAdapter adapter, DbAttribute column) { + NativeColumnType[] variants = adapter.nativeColumnTypes(column.getType()); + if (variants == null || variants.length == 0) { + String entityName = column.getEntity() != null + ? column.getEntity().getFullyQualifiedName() + : "<null>"; + throw new CayenneRuntimeException("Undefined type for attribute '%s.%s': %s." + , entityName, column.getName(), column.getType()); + } + + if (column.isGenerated()) { + for (NativeColumnType variant : variants) { + if (variant.autoIncrement()) { + return variant; + } + } + } else if (isLengthBearingCharacter(column.getType()) && column.getMaxLength() <= 0) { + for (NativeColumnType variant : variants) { + if (variant.unconstrained()) { + return variant; + } + } + } + return variants[0]; + } + public static String sizeAndPrecision(DbAdapter adapter, DbAttribute column) { - if (!adapter.typeSupportsLength(column.getType()) && !adapter.typeSupportsScale(column.getType())) { + int type = column.getType(); + + // an unconstrained character column either uses a length-free native type, or falls back to the + // adapter's default length for databases that require one + if (isLengthBearingCharacter(type) && column.getMaxLength() <= 0) { + return selectNativeType(adapter, column).unconstrained() ? "" : "(" + adapter.defaultCharLength() + ")"; + } + + if (!adapter.typeSupportsLength(type) && !adapter.typeSupportsScale(type)) { return ""; } @@ -401,21 +452,12 @@ public class JdbcAdapter implements DbAdapter { } public static String getType(DbAdapter adapter, DbAttribute column) { - int columnType = column.getType(); - if(columnType == Types.OTHER) { + if (column.getType() == Types.OTHER) { // TODO: warn that this is unsupported yet return "OTHER"; } - NativeColumnType[] types = adapter.nativeColumnTypes(columnType); - if (types == null || types.length == 0) { - String entityName = column.getEntity() != null - ? column.getEntity().getFullyQualifiedName() - : "<null>"; - throw new CayenneRuntimeException("Undefined type for attribute '%s.%s': %s." - , entityName, column.getName(), column.getType()); - } - return types[0].nativeType(); + return selectNativeType(adapter, column).nativeType(); } /** @@ -684,4 +726,7 @@ public class JdbcAdapter implements DbAdapter { return this; } + private static boolean isLengthBearingCharacter(int type) { + return type == Types.CHAR || type == Types.NCHAR || type == Types.VARCHAR || type == Types.NVARCHAR; + } } diff --git a/cayenne/src/main/java/org/apache/cayenne/dba/NativeColumnType.java b/cayenne/src/main/java/org/apache/cayenne/dba/NativeColumnType.java index 3d0c347f3..56d1545f1 100644 --- a/cayenne/src/main/java/org/apache/cayenne/dba/NativeColumnType.java +++ b/cayenne/src/main/java/org/apache/cayenne/dba/NativeColumnType.java @@ -20,18 +20,19 @@ package org.apache.cayenne.dba; /** * Describes a single database-native (external) SQL type that a JDBC type (see {@link java.sql.Types}) maps to. - * A JDBC type may map to more than one external type, e.g. a primary name plus the auto-increment variant used - * for generated columns (PostgreSQL "serial"). + * A JDBC type may map to more than one native type, e.g. a primary name plus the auto-increment variant used + * for generated columns (PostgreSQL "serial"), or a primary name plus an unconstrained variant used for a + * character column with no max length (PostgreSQL "text"). * * @since 5.0 */ -public record NativeColumnType(int jdbcType, String nativeType, boolean autoIncrement) { +public record NativeColumnType(int jdbcType, String nativeType, boolean autoIncrement, boolean unconstrained) { /** - * Creates a plain external type. + * Creates a plain native type. */ - public static NativeColumnType of(int jdbcType, String dbType) { - return new NativeColumnType(jdbcType, dbType, false); + public static NativeColumnType of(int jdbcType, String nativeType) { + return new NativeColumnType(jdbcType, nativeType, false, false); } /** @@ -39,6 +40,14 @@ public record NativeColumnType(int jdbcType, String nativeType, boolean autoIncr * {@link java.sql.Types#INTEGER}. Used to render generated columns. */ public NativeColumnType asAutoIncrement() { - return new NativeColumnType(jdbcType, nativeType, true); + return new NativeColumnType(jdbcType, nativeType, true, unconstrained); + } + + /** + * Returns a copy of this type flagged as the unconstrained variant, i.e. one that may be used for a character + * column with no max length and rendered without a length suffix, e.g. PostgreSQL "text" or MySQL "longtext". + */ + public NativeColumnType asUnconstrained() { + return new NativeColumnType(jdbcType, nativeType, autoIncrement, true); } } diff --git a/cayenne/src/main/java/org/apache/cayenne/dba/h2/H2Adapter.java b/cayenne/src/main/java/org/apache/cayenne/dba/h2/H2Adapter.java index 3e3ace416..2f9de43b7 100644 --- a/cayenne/src/main/java/org/apache/cayenne/dba/h2/H2Adapter.java +++ b/cayenne/src/main/java/org/apache/cayenne/dba/h2/H2Adapter.java @@ -64,7 +64,7 @@ public class H2Adapter extends JdbcAdapter { NativeColumnType.of(Types.BIT, "BIT"), NativeColumnType.of(Types.BLOB, "BLOB"), NativeColumnType.of(Types.BOOLEAN, "BOOLEAN"), - NativeColumnType.of(Types.CHAR, "CHAR"), + NativeColumnType.of(Types.CHAR, "CHAR").asUnconstrained(), NativeColumnType.of(Types.CLOB, "CLOB"), NativeColumnType.of(Types.DATALINK, "DATALINK"), NativeColumnType.of(Types.DATE, "DATE"), @@ -76,10 +76,10 @@ public class H2Adapter extends JdbcAdapter { NativeColumnType.of(Types.LONGNVARCHAR, "NCLOB"), NativeColumnType.of(Types.LONGVARBINARY, "LONGVARBINARY"), NativeColumnType.of(Types.LONGVARCHAR, "CLOB"), - NativeColumnType.of(Types.NCHAR, "NCHAR"), + NativeColumnType.of(Types.NCHAR, "NCHAR").asUnconstrained(), NativeColumnType.of(Types.NCLOB, "NCLOB"), NativeColumnType.of(Types.NUMERIC, "NUMERIC"), - NativeColumnType.of(Types.NVARCHAR, "NVARCHAR"), + NativeColumnType.of(Types.NVARCHAR, "NVARCHAR").asUnconstrained(), NativeColumnType.of(Types.OTHER, "OTHER"), NativeColumnType.of(Types.REAL, "REAL"), NativeColumnType.of(Types.REF, "REF"), @@ -90,7 +90,7 @@ public class H2Adapter extends JdbcAdapter { NativeColumnType.of(Types.TIMESTAMP, "TIMESTAMP"), NativeColumnType.of(Types.TINYINT, "TINYINT"), NativeColumnType.of(Types.VARBINARY, "VARBINARY"), - NativeColumnType.of(Types.VARCHAR, "VARCHAR"), + NativeColumnType.of(Types.VARCHAR, "VARCHAR").asUnconstrained(), }; } diff --git a/cayenne/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapter.java b/cayenne/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapter.java index 103517e8c..0e031871c 100644 --- a/cayenne/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapter.java +++ b/cayenne/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapter.java @@ -76,7 +76,7 @@ public class HSQLDBAdapter extends JdbcAdapter { NativeColumnType.of(Types.BIT, "BIT"), NativeColumnType.of(Types.BLOB, "LONGVARBINARY"), NativeColumnType.of(Types.BOOLEAN, "BOOLEAN"), - NativeColumnType.of(Types.CHAR, "CHAR"), + NativeColumnType.of(Types.CHAR, "CHAR").asUnconstrained(), NativeColumnType.of(Types.CLOB, "LONGVARCHAR"), NativeColumnType.of(Types.DATALINK, "DATALINK"), NativeColumnType.of(Types.DATE, "DATE"), @@ -88,10 +88,10 @@ public class HSQLDBAdapter extends JdbcAdapter { NativeColumnType.of(Types.LONGNVARCHAR, "LONGVARCHAR"), NativeColumnType.of(Types.LONGVARBINARY, "LONGVARBINARY"), NativeColumnType.of(Types.LONGVARCHAR, "LONGVARCHAR"), - NativeColumnType.of(Types.NCHAR, "CHAR"), + NativeColumnType.of(Types.NCHAR, "CHAR").asUnconstrained(), NativeColumnType.of(Types.NCLOB, "LONGVARCHAR"), NativeColumnType.of(Types.NUMERIC, "NUMERIC"), - NativeColumnType.of(Types.NVARCHAR, "VARCHAR"), + NativeColumnType.of(Types.NVARCHAR, "VARCHAR").asUnconstrained(), NativeColumnType.of(Types.OTHER, "OTHER"), NativeColumnType.of(Types.REAL, "REAL"), NativeColumnType.of(Types.REF, "REF"), @@ -101,7 +101,7 @@ public class HSQLDBAdapter extends JdbcAdapter { NativeColumnType.of(Types.TIMESTAMP, "TIMESTAMP"), NativeColumnType.of(Types.TINYINT, "TINYINT"), NativeColumnType.of(Types.VARBINARY, "VARBINARY"), - NativeColumnType.of(Types.VARCHAR, "VARCHAR"), + NativeColumnType.of(Types.VARCHAR, "VARCHAR").asUnconstrained(), }; } diff --git a/cayenne/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java b/cayenne/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java index 32b8ef853..7a5edb83d 100644 --- a/cayenne/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java +++ b/cayenne/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java @@ -91,6 +91,7 @@ public class MySQLAdapter extends JdbcAdapter { NativeColumnType.of(Types.BLOB, "LONGBLOB"), NativeColumnType.of(Types.BOOLEAN, "BOOL"), NativeColumnType.of(Types.CHAR, "CHAR"), + NativeColumnType.of(Types.CHAR, "longtext").asUnconstrained(), NativeColumnType.of(Types.CLOB, "LONGTEXT"), NativeColumnType.of(Types.DATE, "DATE"), NativeColumnType.of(Types.DECIMAL, "DECIMAL"), @@ -101,9 +102,11 @@ public class MySQLAdapter extends JdbcAdapter { NativeColumnType.of(Types.LONGVARBINARY, "LONGBLOB"), NativeColumnType.of(Types.LONGVARCHAR, "LONGTEXT"), NativeColumnType.of(Types.NCHAR, "CHAR"), + NativeColumnType.of(Types.NCHAR, "longtext").asUnconstrained(), NativeColumnType.of(Types.NCLOB, "LONGTEXT"), NativeColumnType.of(Types.NUMERIC, "DECIMAL"), NativeColumnType.of(Types.NVARCHAR, "VARCHAR"), + NativeColumnType.of(Types.NVARCHAR, "longtext").asUnconstrained(), NativeColumnType.of(Types.REAL, "DOUBLE"), NativeColumnType.of(Types.SMALLINT, "SMALLINT"), NativeColumnType.of(Types.SQLXML, "LONGTEXT"), @@ -112,6 +115,7 @@ public class MySQLAdapter extends JdbcAdapter { NativeColumnType.of(Types.TINYINT, "TINYINT"), NativeColumnType.of(Types.VARBINARY, "VARBINARY"), NativeColumnType.of(Types.VARCHAR, "VARCHAR"), + NativeColumnType.of(Types.VARCHAR, "longtext").asUnconstrained(), }; } diff --git a/cayenne/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java b/cayenne/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java index f904e6ed0..4f7da00be 100644 --- a/cayenne/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java +++ b/cayenne/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java @@ -162,6 +162,9 @@ public class OracleAdapter extends JdbcAdapter { // enable batch updates by default setSupportsBatchUpdates(true); + + // VARCHAR2 standard maximum, used for an unconstrained character column + this.defaultCharLength = 4000; } @Override diff --git a/cayenne/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java b/cayenne/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java index 732807965..90dc06fdb 100644 --- a/cayenne/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java +++ b/cayenne/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java @@ -78,6 +78,7 @@ public class PostgresAdapter extends JdbcAdapter { NativeColumnType.of(Types.BLOB, "oid"), NativeColumnType.of(Types.BOOLEAN, "boolean"), NativeColumnType.of(Types.CHAR, "character"), + NativeColumnType.of(Types.CHAR, "text").asUnconstrained(), NativeColumnType.of(Types.CLOB, "text"), NativeColumnType.of(Types.DATE, "date"), NativeColumnType.of(Types.DECIMAL, "decimal"), @@ -89,9 +90,11 @@ public class PostgresAdapter extends JdbcAdapter { NativeColumnType.of(Types.LONGVARBINARY, "bytea"), NativeColumnType.of(Types.LONGVARCHAR, "text"), NativeColumnType.of(Types.NCHAR, "character"), + NativeColumnType.of(Types.NCHAR, "text").asUnconstrained(), NativeColumnType.of(Types.NCLOB, "text"), NativeColumnType.of(Types.NUMERIC, "numeric"), NativeColumnType.of(Types.NVARCHAR, "varchar"), + NativeColumnType.of(Types.NVARCHAR, "text").asUnconstrained(), NativeColumnType.of(Types.OTHER, "json"), NativeColumnType.of(Types.REAL, "real"), NativeColumnType.of(Types.SMALLINT, "smallint"), @@ -102,6 +105,7 @@ public class PostgresAdapter extends JdbcAdapter { NativeColumnType.of(Types.TINYINT, "smallint"), NativeColumnType.of(Types.VARBINARY, "bytea"), NativeColumnType.of(Types.VARCHAR, "varchar"), + NativeColumnType.of(Types.VARCHAR, "text").asUnconstrained(), }; } @@ -149,7 +153,8 @@ public class PostgresAdapter extends JdbcAdapter { @Override public DbAttribute buildAttribute(String name, String typeName, int type, int maxLength, int scale, boolean allowNulls) { - int amendedType = switch (typeName != null ? typeName.toLowerCase() : "") { + String lower = typeName != null ? typeName.toLowerCase() : ""; + int amendedType = switch (lower) { case "json" -> Types.OTHER; // "bytea" maps to pretty much any binary type, so it is up to us to select the most sensible default. // And the winner is LONGVARBINARY @@ -159,7 +164,10 @@ public class PostgresAdapter extends JdbcAdapter { default -> type; }; - return super.buildAttribute(name, typeName, amendedType, maxLength, scale, allowNulls); + // "text" is an unconstrained VARCHAR; the driver reports a bogus huge length, so treat it as unknown (-1) + int amendedLength = "text".equals(lower) ? -1 : maxLength; + + return super.buildAttribute(name, typeName, amendedType, amendedLength, scale, allowNulls); } @Override @@ -234,26 +242,10 @@ public class PostgresAdapter extends JdbcAdapter { , ent.getFullyQualifiedName(), at.getName()); } - NativeColumnType[] types = nativeColumnTypes(at.getType()); - if (types == null || types.length == 0) { - throw new CayenneRuntimeException( - "Undefined type for attribute '%s.%s': %s", ent.getFullyQualifiedName(), at.getName(), at.getType()); - } - - // For a generated column use the auto-increment variant if there is one (serial, bigserial, smallserial). - NativeColumnType selected = types[0]; - if (at.isGenerated()) { - for (NativeColumnType candidate : types) { - if (candidate.autoIncrement()) { - selected = candidate; - break; - } - } - } - + // getType / sizeAndPrecision pick the right variant (auto-increment serial, unconstrained "text", ...) buf.append(context.quotedName(at)) .append(' ') - .append(selected.nativeType()) + .append(getType(this, at)) .append(sizeAndPrecision(this, at)) .append(at.isMandatory() ? " NOT" : "") .append(" NULL"); diff --git a/cayenne/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java b/cayenne/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java index 41556779c..a93235110 100644 --- a/cayenne/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java +++ b/cayenne/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java @@ -90,6 +90,7 @@ public class SQLServerAdapter extends JdbcAdapter { NativeColumnType.of(Types.BLOB, "image"), NativeColumnType.of(Types.BOOLEAN, "bit"), NativeColumnType.of(Types.CHAR, "char"), + NativeColumnType.of(Types.CHAR, "varchar(max)").asUnconstrained(), NativeColumnType.of(Types.CLOB, "text"), NativeColumnType.of(Types.DATE, "date"), NativeColumnType.of(Types.DECIMAL, "decimal"), @@ -100,9 +101,11 @@ public class SQLServerAdapter extends JdbcAdapter { NativeColumnType.of(Types.LONGVARBINARY, "image"), NativeColumnType.of(Types.LONGVARCHAR, "text"), NativeColumnType.of(Types.NCHAR, "nchar"), + NativeColumnType.of(Types.NCHAR, "nvarchar(max)").asUnconstrained(), NativeColumnType.of(Types.NCLOB, "ntext"), NativeColumnType.of(Types.NUMERIC, "numeric"), NativeColumnType.of(Types.NVARCHAR, "nvarchar"), + NativeColumnType.of(Types.NVARCHAR, "nvarchar(max)").asUnconstrained(), NativeColumnType.of(Types.REAL, "real"), NativeColumnType.of(Types.ROWID, "ROWID"), NativeColumnType.of(Types.SMALLINT, "smallint"), @@ -112,6 +115,7 @@ public class SQLServerAdapter extends JdbcAdapter { NativeColumnType.of(Types.TINYINT, "tinyint"), NativeColumnType.of(Types.VARBINARY, "varbinary"), NativeColumnType.of(Types.VARCHAR, "varchar"), + NativeColumnType.of(Types.VARCHAR, "varchar(max)").asUnconstrained(), }; } diff --git a/cayenne/src/test/java/org/apache/cayenne/dba/JdbcAdapterIT.java b/cayenne/src/test/java/org/apache/cayenne/dba/JdbcAdapterIT.java index e25f92a76..ce4850a84 100644 --- a/cayenne/src/test/java/org/apache/cayenne/dba/JdbcAdapterIT.java +++ b/cayenne/src/test/java/org/apache/cayenne/dba/JdbcAdapterIT.java @@ -62,6 +62,19 @@ public class JdbcAdapterIT { assertFalse(types[0].autoIncrement()); } + @Test + public void createTableUnconstrainedVarcharUsesDefaultLength() { + JdbcAdapter adapter = env.adhocObjectFactory().newInstance( + JdbcAdapter.class, + JdbcAdapter.class.getName()); + DbEntity e = new DbEntity("Test"); + DbAttribute c = new DbAttribute("c"); + c.setType(Types.VARCHAR); // no max length -> default char length + e.addAttribute(c); + + assertEquals("CREATE TABLE Test (c VARCHAR(255) NULL)", adapter.createTable(e)); + } + @Test public void createTableQuoteSqlIdentifiers() { diff --git a/cayenne/src/test/java/org/apache/cayenne/dba/NativeColumnTypeTest.java b/cayenne/src/test/java/org/apache/cayenne/dba/NativeColumnTypeTest.java index 7999d5cde..d70b9532b 100644 --- a/cayenne/src/test/java/org/apache/cayenne/dba/NativeColumnTypeTest.java +++ b/cayenne/src/test/java/org/apache/cayenne/dba/NativeColumnTypeTest.java @@ -34,6 +34,7 @@ public class NativeColumnTypeTest { assertEquals(Types.VARCHAR, type.jdbcType()); assertEquals("varchar", type.nativeType()); assertFalse(type.autoIncrement()); + assertFalse(type.unconstrained()); } @Test @@ -43,10 +44,26 @@ public class NativeColumnTypeTest { // only the flag flips; everything else is preserved assertTrue(auto.autoIncrement()); + assertFalse(auto.unconstrained()); assertEquals(base.jdbcType(), auto.jdbcType()); assertEquals(base.nativeType(), auto.nativeType()); // the original is unchanged assertFalse(base.autoIncrement()); } + + @Test + public void asUnconstrained() { + NativeColumnType base = NativeColumnType.of(Types.VARCHAR, "text"); + NativeColumnType unconstrained = base.asUnconstrained(); + + // only the flag flips; everything else is preserved + assertTrue(unconstrained.unconstrained()); + assertFalse(unconstrained.autoIncrement()); + assertEquals(base.jdbcType(), unconstrained.jdbcType()); + assertEquals(base.nativeType(), unconstrained.nativeType()); + + // the original is unchanged + assertFalse(base.unconstrained()); + } } diff --git a/cayenne/src/test/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapterIT.java b/cayenne/src/test/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapterIT.java index 8b80f6d58..546a7f1fc 100644 --- a/cayenne/src/test/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapterIT.java +++ b/cayenne/src/test/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapterIT.java @@ -66,7 +66,22 @@ public class HSQLDBAdapterIT { e.addAttribute(dblPrec); String sql = adapter.createTable(e); - + assertEquals(0,sql.indexOf("CREATE CACHED TABLE")); } + + @Test + public void createTableUnconstrainedVarcharIsLengthFree() { + HSQLDBAdapter adapter = env.adhocObjectFactory().newInstance( + HSQLDBAdapter.class, + HSQLDBAdapter.class.getName()); + DbEntity e = new DbEntity("Test"); + DbAttribute c = new DbAttribute("c"); + c.setType(Types.VARCHAR); // no max length -> unconstrained + + e.addAttribute(c); + + // HSQLDB accepts a length-free VARCHAR + assertEquals("CREATE CACHED TABLE Test (c VARCHAR NULL)", adapter.createTable(e)); + } } diff --git a/cayenne/src/test/java/org/apache/cayenne/dba/mysql/MySQLAdapterIT.java b/cayenne/src/test/java/org/apache/cayenne/dba/mysql/MySQLAdapterIT.java index 7613d54bf..6b13183aa 100644 --- a/cayenne/src/test/java/org/apache/cayenne/dba/mysql/MySQLAdapterIT.java +++ b/cayenne/src/test/java/org/apache/cayenne/dba/mysql/MySQLAdapterIT.java @@ -115,4 +115,18 @@ public class MySQLAdapterIT { assertTrue(sql.indexOf("DATETIME") > 0); assertEquals("CREATE TABLE Test (dbl1 DATETIME NULL, dbl2 TIME NULL) ENGINE=InnoDB", sql); } + + @Test + public void createTableUnconstrainedVarcharRendersLongtext() { + MySQLAdapter adapter = env.adhocObjectFactory().newInstance( + MySQLAdapter.class, + MySQLAdapter.class.getName()); + + DbEntity e = new DbEntity("Test"); + DbAttribute c = new DbAttribute("c"); + c.setType(Types.VARCHAR); // no max length -> unconstrained + e.addAttribute(c); + + assertEquals("CREATE TABLE Test (c longtext NULL) ENGINE=InnoDB", adapter.createTable(e)); + } } diff --git a/cayenne/src/test/java/org/apache/cayenne/dba/oracle/OracleAdapterIT.java b/cayenne/src/test/java/org/apache/cayenne/dba/oracle/OracleAdapterIT.java index 7f240469d..57258fd1b 100644 --- a/cayenne/src/test/java/org/apache/cayenne/dba/oracle/OracleAdapterIT.java +++ b/cayenne/src/test/java/org/apache/cayenne/dba/oracle/OracleAdapterIT.java @@ -21,6 +21,8 @@ package org.apache.cayenne.dba.oracle; import org.apache.cayenne.dba.NativeColumnType; import org.apache.cayenne.map.DataMap; +import org.apache.cayenne.map.DbAttribute; +import org.apache.cayenne.map.DbEntity; import org.apache.cayenne.query.InsertBatchQuery; import org.apache.cayenne.unit.CayenneProjects; import org.apache.cayenne.unit.CayenneTestsEnv; @@ -62,4 +64,19 @@ public class OracleAdapterIT { assertEquals(1, types.length); assertEquals("TIMESTAMP", types[0].nativeType()); } + + @Test + public void createTableUnconstrainedVarcharUsesDefaultLength() { + OracleAdapter adapter = env.adhocObjectFactory().newInstance( + OracleAdapter.class, + OracleAdapter.class.getName()); + DbEntity e = new DbEntity("Test"); + DbAttribute c = new DbAttribute("c"); + c.setType(Types.VARCHAR); // no max length -> Oracle default char length + + e.addAttribute(c); + + // Oracle has no length-free character type, so it falls back to VARCHAR2(4000) + assertEquals("CREATE TABLE Test (c VARCHAR2(4000) NULL)", adapter.createTable(e)); + } } diff --git a/cayenne/src/test/java/org/apache/cayenne/dba/postgres/PostgresAdapterIT.java b/cayenne/src/test/java/org/apache/cayenne/dba/postgres/PostgresAdapterIT.java index 7b147bdef..9795c56b4 100644 --- a/cayenne/src/test/java/org/apache/cayenne/dba/postgres/PostgresAdapterIT.java +++ b/cayenne/src/test/java/org/apache/cayenne/dba/postgres/PostgresAdapterIT.java @@ -94,4 +94,29 @@ public class PostgresAdapterIT { assertArrayEquals(new String[]{"integer", "serial"}, adapter.externalTypesForJdbcType(Types.INTEGER)); } + @Test + public void textColumnReverseEngineeredAsUnconstrained() { + PostgresAdapter adapter = env.adhocObjectFactory().newInstance( + PostgresAdapter.class, + PostgresAdapter.class.getName()); + + // the driver reports a bogus huge length for "text"; it must become an unconstrained VARCHAR + DbAttribute attribute = adapter.buildAttribute("c", "text", Types.VARCHAR, Integer.MAX_VALUE, -1, true); + assertEquals(Types.VARCHAR, attribute.getType()); + assertEquals(-1, attribute.getMaxLength()); + } + + @Test + public void createTableUnconstrainedVarcharRendersText() { + PostgresAdapter adapter = env.adhocObjectFactory().newInstance( + PostgresAdapter.class, + PostgresAdapter.class.getName()); + DbEntity e = new DbEntity("Test"); + DbAttribute c = new DbAttribute("c"); + c.setType(Types.VARCHAR); // no max length -> unconstrained + e.addAttribute(c); + + assertEquals("CREATE TABLE Test (c text NULL)", adapter.createTable(e)); + } + } diff --git a/cayenne/src/test/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapterIT.java b/cayenne/src/test/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapterIT.java index 6843d9582..20245537b 100644 --- a/cayenne/src/test/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapterIT.java +++ b/cayenne/src/test/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapterIT.java @@ -55,4 +55,17 @@ public class SQLServerAdapterIT { assertEquals(-1, sql.indexOf("float(22, 12)")); assertEquals("CREATE TABLE Test (dbl1 float(22) NULL)", sql); } + + @Test + public void createTableUnconstrainedVarcharRendersMax() { + SQLServerAdapter adapter = env.adhocObjectFactory().newInstance( + SQLServerAdapter.class, + SQLServerAdapter.class.getName()); + DbEntity e = new DbEntity("Test"); + DbAttribute c = new DbAttribute("c"); + c.setType(Types.VARCHAR); // no max length -> unconstrained + e.addAttribute(c); + + assertEquals("CREATE TABLE Test (c varchar(max) NULL)", adapter.createTable(e)); + } } diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ui/project/editor/datadomain/validation/InspectionCheckBoxTree.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ui/project/editor/datadomain/validation/InspectionCheckBoxTree.java index 224413257..30847a2d6 100644 --- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ui/project/editor/datadomain/validation/InspectionCheckBoxTree.java +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ui/project/editor/datadomain/validation/InspectionCheckBoxTree.java @@ -68,6 +68,9 @@ class InspectionCheckBoxTree extends CheckBoxTree { DefaultMutableTreeNode root = new DefaultMutableTreeNode(); Map<Inspection.Group, MutableTreeNode> groupNodes = new EnumMap<>(Inspection.Group.class); for (Inspection inspection : Inspection.values()) { + if (inspection.isDeprecated()) { + continue; + } MutableTreeNode groupNode = groupNodes.computeIfAbsent(inspection.group(), g -> addChild(root, g)); DefaultMutableTreeNode inspectionNode = addChild(groupNode, inspection); inspectionPathCache.put(inspection, new TreePath(inspectionNode.getPath()));
