diff --git a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConfiguration.java b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConfiguration.java index 43d282e0..d0c19542 100644 --- a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConfiguration.java +++ b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConfiguration.java @@ -25,6 +25,9 @@ import static org.identityconnectors.databasetable.DatabaseTableConstants.*; +import java.util.Arrays; +import java.util.Set; +import org.identityconnectors.common.CollectionUtil; import org.identityconnectors.common.StringUtil; import org.identityconnectors.common.logging.Log; import org.identityconnectors.common.security.GuardedString; @@ -737,6 +740,68 @@ public void setLastLoginDateColumn(String lastLoginDateColumn) { this.lastLoginDateColumn = lastLoginDateColumn; } + /** + * Multivalue columns. Column names listed here are treated as multivalue: the single DB string + * value is parsed into multiple ICF attribute values on read. Only String columns are supported. + */ + private String[] multivalueColumns; + + @ConfigurationProperty(order = 33, + displayMessageKey = "MULTIVALUE_COLUMNS_DISPLAY", + helpMessageKey = "MULTIVALUE_COLUMNS_HELP") + public String[] getMultivalueColumns() { + return multivalueColumns; + } + + public void setMultivalueColumns(String[] multivalueColumns) { + this.multivalueColumns = multivalueColumns; + } + + /** + * Fully-qualified class name of a custom {@code MultivalueParser} implementation. + * If blank, the default single-character split parser is used. + */ + private String multivalueParser = EMPTY_STR; + + @ConfigurationProperty(order = 34, + displayMessageKey = "MULTIVALUE_PARSER_DISPLAY", + helpMessageKey = "MULTIVALUE_PARSER_HELP") + public String getMultivalueParser() { + return multivalueParser; + } + + public void setMultivalueParser(String multivalueParser) { + this.multivalueParser = multivalueParser; + } + + /** + * Configuration string passed to the multivalue parser. + * For the default parser this is the single separator character (default: {@code |}). + */ + private String multivalueParserConfig = EMPTY_STR; + + @ConfigurationProperty(order = 35, + displayMessageKey = "MULTIVALUE_PARSER_CONFIG_DISPLAY", + helpMessageKey = "MULTIVALUE_PARSER_CONFIG_HELP") + public String getMultivalueParserConfig() { + return multivalueParserConfig; + } + + public void setMultivalueParserConfig(String multivalueParserConfig) { + this.multivalueParserConfig = multivalueParserConfig; + } + + /** + * Returns a case-insensitive set of multivalue column names. Returns empty set if none configured. + */ + public Set getMultivalueColumnsSet() { + Set result = CollectionUtil.newCaseInsensitiveSet(); + if (multivalueColumns != null) { + result.addAll(Arrays.asList(multivalueColumns)); + } + return result; + } + // ======================================================================= // Configuration Interface // ======================================================================= @@ -835,6 +900,13 @@ private void validateConfigurationForTable() { } catch (IllegalArgumentException e) { throw new IllegalArgumentException(getMessage(MSG_INVALID_QUOTING, getQuoting())); } + + if (multivalueColumns != null && multivalueColumns.length > 0 + && StringUtil.isBlank(multivalueParser) + && StringUtil.isNotBlank(multivalueParserConfig) + && multivalueParserConfig.length() != 1) { + throw new IllegalArgumentException(getMessage(MSG_MULTIVALUE_PARSER_CONFIG_INVALID)); + } } /** diff --git a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConnector.java b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConnector.java index 3f404261..e11c3e58 100644 --- a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConnector.java +++ b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConnector.java @@ -38,6 +38,7 @@ import org.identityconnectors.common.security.GuardedString; import org.identityconnectors.databasetable.mapping.MappingStrategy; import org.identityconnectors.databasetable.mapping.misc.SQLColumnTypeInfo; +import org.identityconnectors.databasetable.multivalue.MultivalueParser; import org.identityconnectors.dbcommon.*; import org.identityconnectors.dbcommon.DatabaseQueryBuilder.OrderBy; import org.identityconnectors.framework.common.exceptions.*; @@ -109,6 +110,16 @@ public class DatabaseTableConnector implements PoolableConnector, CreateOp, Sear */ private Set stringColumnRequired; + /** + * Parser for multivalue columns; null if no multivalue columns are configured. + */ + private MultivalueParser multivalueParser; + + /** + * Case-insensitive set of column names that should be treated as multivalue. + */ + private Set multivalueColumnsSet; + // ======================================================================= // Initialize/dispose methods.. // ======================================================================= @@ -130,6 +141,8 @@ public void init(Configuration cfg) { this.schema = null; this.defaultAttributesToGet = null; this.columnSQLTypes = null; + this.multivalueParser = null; + this.multivalueColumnsSet = null; log.ok("init DatabaseTable connector ok, connection is valid"); } @@ -182,6 +195,8 @@ public void dispose() { this.defaultAttributesToGet = null; this.schema = null; this.columnSQLTypes = null; + this.multivalueParser = null; + this.multivalueColumnsSet = null; } /** @@ -936,6 +951,16 @@ public String getColumnName(String attributeName) { * Cache schema, defaultAtributesToGet, columnClassNamens */ private void cacheSchema() { + /* + * Initialize multivalue support before building attribute infos. + */ + multivalueColumnsSet = config.getMultivalueColumnsSet(); + if (!multivalueColumnsSet.isEmpty()) { + multivalueParser = MultivalueParser.getInstance(config); + } else { + multivalueParser = null; + } + /* * First, compute the account attributes based on the database schema */ @@ -1092,6 +1117,11 @@ private Set buildAttributeInfoSet(ResultSet rset) throws SQLExcep stringColumnRequired.add(name); } attrBld.setReturnedByDefault(isReturnedByDefault(dataType)); + if (multivalueColumnsSet != null && multivalueColumnsSet.contains(name)) { + attrBld.setType(String.class); + attrBld.setMultiValued(true); + log.ok("column {0} marked as multivalue in schema", name); + } attrInfo.add(attrBld.build()); log.ok("the column name {0} has data type {1}", name, dataType); } @@ -1167,7 +1197,10 @@ private ConnectorObjectBuilder buildConnectorObject(Map column if (param != null && param.getValue() != null) { Object paramValue = param.getValue(); - if (!(paramValue instanceof UUID)) { + if (multivalueColumnsSet != null && multivalueColumnsSet.contains(columnName)) { + Collection values = multivalueParser.parse(paramValue.toString()); + bld.addAttribute(AttributeBuilder.build(columnName, values)); + } else if (!(paramValue instanceof UUID)) { bld.addAttribute(AttributeBuilder.build(columnName, paramValue)); } else { diff --git a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConstants.java b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConstants.java index f3f68e2d..fedf0f2d 100644 --- a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConstants.java +++ b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConstants.java @@ -75,6 +75,7 @@ public class DatabaseTableConstants { static final String MSG_EXP_DEFAULT = "exception.default"; static final String MSG_EXP_UNKNOWN_UID = "exception.unknown.uid"; static final String MSG_EXP_TOO_MANY_UID = "exception.more.than.one.uid"; + static final String MSG_MULTIVALUE_PARSER_CONFIG_INVALID = "multivalue.parser.config.invalid"; // public static final String DEFAULT_SQLSTATE_UNIQUE_CONSTRAIN_VIOLATION = "23505"; // public static final String DEFAULT_SQLSTATE_INTEGRITY_CONSTRAIN_VIOLATION = "23000"; diff --git a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/multivalue/DefaultMultivalueParser.java b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/multivalue/DefaultMultivalueParser.java new file mode 100644 index 00000000..dcfcfb29 --- /dev/null +++ b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/multivalue/DefaultMultivalueParser.java @@ -0,0 +1,66 @@ +/* + * ==================== + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2008-2009 Sun Microsystems, Inc. All rights reserved. + * + * The contents of this file are subject to the terms of the Common Development + * and Distribution License("CDDL") (the "License"). You may not use this file + * except in compliance with the License. + * + * You can obtain a copy of the License at + * http://IdentityConnectors.dev.java.net/legal/license.txt + * See the License for the specific language governing permissions and limitations + * under the License. + * + * When distributing the Covered Code, include this CDDL Header Notice in each file + * and include the License file at identityconnectors/legal/license.txt. + * If applicable, add the following below this CDDL Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * ==================== + * Portions Copyrighted 2013-2022 Evolveum + */ +package org.identityconnectors.databasetable.multivalue; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.identityconnectors.databasetable.DatabaseTableConfiguration; + +/** + * Default multivalue parser: splits a string on a single configurable separator character. + * The separator defaults to {@code |} if not configured. + */ +public class DefaultMultivalueParser implements MultivalueParser { + + private final char separator; + + public DefaultMultivalueParser(char separator) { + this.separator = separator; + } + + public static MultivalueParser getInstance(DatabaseTableConfiguration cfg) { + String parserConfig = cfg.getMultivalueParserConfig(); + char sep = (parserConfig != null && !parserConfig.isEmpty()) ? parserConfig.charAt(0) : '|'; + return new DefaultMultivalueParser(sep); + } + + @Override + public Collection parse(String dbColumnValue) { + if (dbColumnValue == null) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + int start = 0; + for (int i = 0; i < dbColumnValue.length(); i++) { + if (dbColumnValue.charAt(i) == separator) { + result.add(dbColumnValue.substring(start, i)); + start = i + 1; + } + } + result.add(dbColumnValue.substring(start)); + return result; + } +} diff --git a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/multivalue/MultivalueParser.java b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/multivalue/MultivalueParser.java new file mode 100644 index 00000000..91e3b718 --- /dev/null +++ b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/multivalue/MultivalueParser.java @@ -0,0 +1,61 @@ +/* + * ==================== + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2008-2009 Sun Microsystems, Inc. All rights reserved. + * + * The contents of this file are subject to the terms of the Common Development + * and Distribution License("CDDL") (the "License"). You may not use this file + * except in compliance with the License. + * + * You can obtain a copy of the License at + * http://IdentityConnectors.dev.java.net/legal/license.txt + * See the License for the specific language governing permissions and limitations + * under the License. + * + * When distributing the Covered Code, include this CDDL Header Notice in each file + * and include the License file at identityconnectors/legal/license.txt. + * If applicable, add the following below this CDDL Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * ==================== + * Portions Copyrighted 2013-2022 Evolveum + */ +package org.identityconnectors.databasetable.multivalue; + +import java.util.Collection; +import org.identityconnectors.databasetable.DatabaseTableConfiguration; + +/** + * Parser interface for multivalue column support. + * Implementations parse a single DB column string value into multiple ICF attribute values. + */ +public interface MultivalueParser { + + /** + * Factory method: called once per connector init. Returns a stateless parser. + * If {@code cfg.getMultivalueParser()} is blank, returns a {@link DefaultMultivalueParser}. + * Otherwise, reflectively invokes {@code getInstance(DatabaseTableConfiguration)} on the named class. + */ + static MultivalueParser getInstance(DatabaseTableConfiguration cfg) { + String fqcn = cfg.getMultivalueParser(); + if (fqcn == null || fqcn.isBlank()) { + return DefaultMultivalueParser.getInstance(cfg); + } + try { + Class cls = Class.forName(fqcn); + return (MultivalueParser) cls.getMethod("getInstance", DatabaseTableConfiguration.class) + .invoke(null, cfg); + } catch (Exception e) { + throw new IllegalArgumentException("Cannot instantiate MultivalueParser: " + fqcn, e); + } + } + + /** + * Parse one DB column string value into multiple ICF attribute values. + * + * @param dbColumnValue the raw string value from the database column + * @return a collection of parsed string values + */ + Collection parse(String dbColumnValue); +} diff --git a/connectors/java/databasetable/src/main/resources/org/identityconnectors/databasetable/Messages.properties b/connectors/java/databasetable/src/main/resources/org/identityconnectors/databasetable/Messages.properties index 8ad9daaf..77071d4e 100644 --- a/connectors/java/databasetable/src/main/resources/org/identityconnectors/databasetable/Messages.properties +++ b/connectors/java/databasetable/src/main/resources/org/identityconnectors/databasetable/Messages.properties @@ -120,4 +120,11 @@ SQL_STATE_INVALID_ATTRIBUTE_VALUE_HELP=Collection of values representing SQL sta SQL_STATE_CONFIGURATION_EXCEPTION_DISPLAY=Configuration Exception SQL state codes SQL_STATE_CONFIGURATION_EXCEPTION_HELP=Collection of values representing SQL state codes which can be interpreted to create an Configuration exception. LAST_LOGIN_DATE_COLUMN_DISPLAY=Last Login Date Column -LAST_LOGIN_DATE_COLUMN_HELP=Enter the name of the column in the table that will hold the last login date values. If empty, last login date capability is disabled. \ No newline at end of file +LAST_LOGIN_DATE_COLUMN_HELP=Enter the name of the column in the table that will hold the last login date values. If empty, last login date capability is disabled. +MULTIVALUE_COLUMNS_DISPLAY=Multivalue Columns +MULTIVALUE_COLUMNS_HELP=Enter the names of columns whose values should be parsed into multiple ICF attribute values on read. Only String/VARCHAR columns are supported. +MULTIVALUE_PARSER_DISPLAY=Multivalue Parser Class +MULTIVALUE_PARSER_HELP=Fully-qualified class name of a custom MultivalueParser implementation. If empty, the default single-character split parser is used. +MULTIVALUE_PARSER_CONFIG_DISPLAY=Multivalue Parser Config +MULTIVALUE_PARSER_CONFIG_HELP=Configuration string for the multivalue parser. For the default parser, this is the single separator character (default: |). +multivalue.parser.config.invalid=The multivalueParserConfig must be a single character when using the default parser. \ No newline at end of file diff --git a/connectors/java/databasetable/src/test/java/org/identityconnectors/databasetable/DatabaseTableDSDerbyTests.java b/connectors/java/databasetable/src/test/java/org/identityconnectors/databasetable/DatabaseTableDSDerbyTests.java index 49ac944a..175262e9 100644 --- a/connectors/java/databasetable/src/test/java/org/identityconnectors/databasetable/DatabaseTableDSDerbyTests.java +++ b/connectors/java/databasetable/src/test/java/org/identityconnectors/databasetable/DatabaseTableDSDerbyTests.java @@ -210,6 +210,7 @@ protected Set getCreateAttributeSet(DatabaseTableConfiguration cfg) t ret.add(AttributeBuilder.build(FIRSTNAME, randomString(r, 50))); ret.add(AttributeBuilder.build(LASTNAME, randomString(r, 50))); ret.add(AttributeBuilder.build(EMAIL, randomString(r, 50))); + ret.add(AttributeBuilder.build(GROUPS, randomString(r, 50))); ret.add(AttributeBuilder.build(DEPARTMENT, randomString(r, 50))); ret.add(AttributeBuilder.build(TITLE, randomString(r, 50))); if(!cfg.getChangeLogColumn().equalsIgnoreCase(AGE)){ @@ -269,7 +270,7 @@ public void testNoZeroSQLExceptions() throws Exception { final ExpectProxy smse = new ExpectProxy(); MappingStrategy sms = smse.getProxy(MappingStrategy.class); //Schema - for (int i = 0; i < 15; i++) { + for (int i = 0; i < 16; i++) { smse.expectAndReturn("getSQLAttributeType", String.class); } //Create fail @@ -295,7 +296,7 @@ public void testNonZeroSQLExceptions() throws Exception { final ExpectProxy smse = new ExpectProxy(); MappingStrategy sms = smse.getProxy(MappingStrategy.class); - for (int i = 0; i < 15; i++) { + for (int i = 0; i < 16; i++) { smse.expectAndReturn("getSQLAttributeType", String.class); } smse.expectAndThrow("setSQLParam", new SQLException("test reason", "411", 411)); @@ -317,7 +318,7 @@ public void testRethrowAllSQLExceptions() throws Exception { final ExpectProxy smse = new ExpectProxy(); MappingStrategy sms = smse.getProxy(MappingStrategy.class); - for (int i = 0; i < 15; i++) { + for (int i = 0; i < 16; i++) { smse.expectAndReturn("getSQLAttributeType", String.class); } smse.expectAndThrow("setSQLParam", new SQLException("test reason", "0", 0)); diff --git a/connectors/java/databasetable/src/test/java/org/identityconnectors/databasetable/DatabaseTableDerbyTests.java b/connectors/java/databasetable/src/test/java/org/identityconnectors/databasetable/DatabaseTableDerbyTests.java index 6c107181..b63d1e20 100644 --- a/connectors/java/databasetable/src/test/java/org/identityconnectors/databasetable/DatabaseTableDerbyTests.java +++ b/connectors/java/databasetable/src/test/java/org/identityconnectors/databasetable/DatabaseTableDerbyTests.java @@ -45,6 +45,7 @@ import java.sql.Timestamp; import java.sql.Types; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -59,6 +60,7 @@ import org.identityconnectors.dbcommon.SQLParam; import org.identityconnectors.dbcommon.SQLUtil; import org.identityconnectors.framework.common.exceptions.ConnectorException; +import org.identityconnectors.framework.common.objects.filter.FilterBuilder; import org.identityconnectors.test.common.TestHelpers; /** @@ -183,6 +185,7 @@ protected Set getCreateAttributeSet(DatabaseTableConfiguration cfg) t ret.add(AttributeBuilder.build(FIRSTNAME, randomString(r, 50))); ret.add(AttributeBuilder.build(LASTNAME, randomString(r, 50))); ret.add(AttributeBuilder.build(EMAIL, randomString(r, 50))); + ret.add(AttributeBuilder.build(GROUPS, randomString(r, 50))); ret.add(AttributeBuilder.build(DEPARTMENT, randomString(r, 50))); ret.add(AttributeBuilder.build(TITLE, randomString(r, 50))); if (!cfg.getChangeLogColumn().equalsIgnoreCase(AGE)) { @@ -252,7 +255,7 @@ public void testNoZeroSQLExceptions() throws Exception { final ExpectProxy smse = new ExpectProxy(); MappingStrategy sms = smse.getProxy(MappingStrategy.class); //Schema - for (int i = 0; i < 15; i++) { + for (int i = 0; i < 16; i++) { smse.expectAndReturn("getSQLAttributeType", String.class); } //Create fail @@ -279,7 +282,7 @@ public void testNonZeroSQLExceptions() throws Exception { final ExpectProxy smse = new ExpectProxy(); MappingStrategy sms = smse.getProxy(MappingStrategy.class); - for (int i = 0; i < 15; i++) { + for (int i = 0; i < 16; i++) { smse.expectAndReturn("getSQLAttributeType", String.class); } smse.expectAndThrow("setSQLParam", new SQLException("test reason", "411", 411)); @@ -302,7 +305,7 @@ public void testRethrowAllSQLExceptions() throws Exception { final ExpectProxy smse = new ExpectProxy(); MappingStrategy sms = smse.getProxy(MappingStrategy.class); - for (int i = 0; i < 15; i++) { + for (int i = 0; i < 16; i++) { smse.expectAndReturn("getSQLAttributeType", String.class); } smse.expectAndThrow("setSQLParam", new SQLException("test reason", "0", 0)); @@ -511,4 +514,56 @@ protected ConnectorFacade createNewInstance(DatabaseTableConfiguration config) { APIConfiguration impl = TestHelpers.createTestConfiguration(DatabaseTableConnector.class, config); return factory.newInstance(impl); } + + /** + * Tests that a column declared as multivalue is parsed into multiple ICF attribute values on read. + */ + @Test + public void testMultivalueRead() throws Exception { + DatabaseTableConfiguration cfg = getConfiguration(); + cfg.setMultivalueColumns(new String[]{GROUPS}); + DatabaseTableConnector con = getConnector(cfg); + + deleteAllFromAccounts(con.getConn()); + + // Insert directly via JDBC with pipe-separated groups value + String accountId = "testmv_" + randomString(r, 10); + insertAccountWithGroups(con.getConn().getConnection(), accountId, "admin|devops|readonly"); + con.getConn().commit(); + + // Read back via connector + List results = TestHelpers.searchToList(con, ObjectClass.ACCOUNT, + FilterBuilder.equalTo(new Uid(accountId))); + assertEquals(1, results.size()); + + Attribute groupsAttr = results.get(0).getAttributeByName(GROUPS); + assertNotNull(groupsAttr); + assertEquals(Arrays.asList("admin", "devops", "readonly"), groupsAttr.getValue()); + + // Schema check + Schema schema = con.schema(); + ObjectClassInfo oci = schema.findObjectClassInfo(ObjectClass.ACCOUNT_NAME); + AttributeInfo ai = oci.getAttributeInfo().stream() + .filter(a -> GROUPS.equalsIgnoreCase(a.getName())) + .findFirst().orElse(null); + assertNotNull(ai); + assertTrue(ai.isMultiValued()); + assertEquals(String.class, ai.getType()); + } + + private void insertAccountWithGroups(Connection conn, String accountId, String groups) throws SQLException { + final String sql = "INSERT INTO Accounts (accountId, firstname, lastname, changed, groups) VALUES (?, ?, ?, ?, ?)"; + PreparedStatement ps = null; + try { + ps = conn.prepareStatement(sql); + ps.setString(1, accountId); + ps.setString(2, "Test"); + ps.setString(3, "User"); + ps.setTimestamp(4, new Timestamp(System.currentTimeMillis())); + ps.setString(5, groups); + ps.execute(); + } finally { + SQLUtil.closeQuietly(ps); + } + } } diff --git a/connectors/java/databasetable/src/test/java/org/identityconnectors/databasetable/DatabaseTableTestBase.java b/connectors/java/databasetable/src/test/java/org/identityconnectors/databasetable/DatabaseTableTestBase.java index ae78af57..2c1c5f48 100644 --- a/connectors/java/databasetable/src/test/java/org/identityconnectors/databasetable/DatabaseTableTestBase.java +++ b/connectors/java/databasetable/src/test/java/org/identityconnectors/databasetable/DatabaseTableTestBase.java @@ -66,6 +66,7 @@ public abstract class DatabaseTableTestBase { static final String FIRSTNAME = "firstname"; static final String LASTNAME = "lastname"; static final String EMAIL = "email"; + static final String GROUPS = "groups"; static final String DEPARTMENT = "department"; static final String TITLE = "title"; static final String AGE = "age"; diff --git a/connectors/java/databasetable/src/test/resources/org/identityconnectors/databasetable/derbyTest.sql b/connectors/java/databasetable/src/test/resources/org/identityconnectors/databasetable/derbyTest.sql index d9cfd980..7e40204c 100644 --- a/connectors/java/databasetable/src/test/resources/org/identityconnectors/databasetable/derbyTest.sql +++ b/connectors/java/databasetable/src/test/resources/org/identityconnectors/databasetable/derbyTest.sql @@ -6,6 +6,7 @@ create table Accounts ( firstname VARCHAR(50) NOT NULL, lastname VARCHAR(50) NOT NULL, email VARCHAR(250), + groups VARCHAR(250), department VARCHAR(250), title VARCHAR(250), age INTEGER,