Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
import com.evolveum.polygon.conndev.dev.ConnDevSchema;
import com.evolveum.polygon.conndev.spi.ClassHandlerConnectorBase;
import com.evolveum.polygon.conndev.spi.ObjectClassHandler;
import com.evolveum.polygon.conndev.spi.ObjectCreateOperation;
import com.evolveum.polygon.conndev.spi.ObjectDeleteOperation;
import com.evolveum.polygon.conndev.spi.ObjectSearchOperation;
import com.evolveum.polygon.conndev.spi.ObjectSyncOperation;
import com.evolveum.polygon.conndev.spi.ObjectUpdateOperation;
import com.evolveum.polygon.sql.base.build.api.SqlObjectClassDefinition;
import com.evolveum.polygon.sql.base.build.api.SqlSchemaBuilder;
import com.evolveum.polygon.sql.base.build.api.SqlSchemaBuilderImpl;
Expand All @@ -26,6 +29,9 @@
import com.evolveum.polygon.sql.base.search.SqlSearchOperation;
import com.evolveum.polygon.sql.base.sync.SqlSyncOperation;
import com.evolveum.polygon.sql.base.sync.SyncConfig;
import com.evolveum.polygon.sql.base.write.SqlCreateOperation;
import com.evolveum.polygon.sql.base.write.SqlDeleteOperation;
import com.evolveum.polygon.sql.base.write.SqlUpdateOperation;
import com.querydsl.sql.SQLTemplates;
import org.identityconnectors.framework.common.exceptions.ConnectionFailedException;
import org.identityconnectors.framework.common.exceptions.InvalidCredentialException;
Expand Down Expand Up @@ -206,15 +212,25 @@ private void initialize0(boolean allowConnection) {
ObjectSearchOperation.class, new SqlObjectClassDevHandler(context));
}

// Register QueryDSL-based search and sync operations for all application object classes (tables)
// Register QueryDSL-based operations for all application object classes (tables).
// Explicit Groovy handlers take precedence over these defaults.
if (context.schema() != null) {
for (SqlObjectClassDefinition def : context.schema().objectClasses()) {
var oc = def.objectClass();
var mapping = def.sql();
if (mapping != null) {
handlerBuilder.register(oc, ObjectSearchOperation.class, new SqlSearchOperation(context, def));
handlerBuilder.register(oc, ObjectSyncOperation.class,
new SqlSyncOperation(context, def, SyncConfig.defaultFor(def)));
handlerBuilder.registerIfAbsent(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be necessary after rebase

oc, ObjectSearchOperation.class, new SqlSearchOperation(context, def));
handlerBuilder.registerIfAbsent(oc, ObjectSyncOperation.class,
new SqlSyncOperation(context, def, SyncConfig.defaultFor(def)));
if (!Boolean.TRUE.equals(def.getReadOnly())) {
handlerBuilder.registerIfAbsent(
oc, ObjectCreateOperation.class, new SqlCreateOperation(context, def));
handlerBuilder.registerIfAbsent(
oc, ObjectUpdateOperation.class, new SqlUpdateOperation(context, def));
handlerBuilder.registerIfAbsent(
oc, ObjectDeleteOperation.class, new SqlDeleteOperation(context, def));
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,17 @@ default FilterSupport sqlFilter() {

Collection<Path<?>> selectPaths(Path<?> table);

/**
* Converts a ConnId attribute value into the physical column assignments used by SQL DML.
* A regular attribute produces one assignment; a composite UID produces one per key column.
*/
List<ColumnValue> columnValues(Path<?> table, Object connIdValue);

DefinitionValue<String> column();

record ColumnValue(Path<?> path, Object value) {
}

interface FilterSupport {

BooleanExpression eq(RelationalPathBase<?> tablePath, Object connIdValue);
Expand Down Expand Up @@ -231,6 +240,11 @@ public Collection<Path<?>> selectPaths(Path<?> table) {
return List.of(dslPath(table));
}

@Override
public List<ColumnValue> columnValues(Path<?> table, Object connIdValue) {
return List.of(new ColumnValue(dslPath(table), toSqlValue(connIdValue)));
}

public Object toSqlValue(Object connIdValue) {
return valueMapping().toWireValue(connIdValue);
}
Expand Down Expand Up @@ -301,13 +315,16 @@ public BooleanExpression predicateFor(RelationalPathBase<?> tp, AttributeFilter
@Override
public BooleanExpression eq(RelationalPathBase<?> tp, Object connIdValue) {
if (connIdValue == null) {
var r = (BooleanExpression) self.mainColumn.dslPath(tp);
for (SingleColumn ac : self.additionalColumns) { r = r.and((BooleanExpression) ac.dslPath(tp)); }
return r.isNull();
var result = self.mainColumn.sqlFilter().eq(tp, null);
for (var additionalColumn : self.additionalColumns) {
result = result.and(additionalColumn.sqlFilter().eq(tp, null));
}
return result;
}
// Split composite UID by delimiter.
var uidValue = connIdValue.toString();
var parts = uidValue.split(delimiter, self.additionalColumns.size() + 1);
var parts = uidValue.split(
Pattern.quote(delimiter), self.additionalColumns.size() + 1);
if (parts.length != self.additionalColumns.size() + 1) {
throw new IllegalArgumentException(
"UID has wrong number of parts: expected " + (self.additionalColumns.size() + 1) +
Expand All @@ -332,6 +349,34 @@ public BooleanExpression eq(RelationalPathBase<?> tp, Object connIdValue) {
return paths;
}

@Override
public List<ColumnValue> columnValues(Path<?> table, Object connIdValue) {
var columns = new ArrayList<ColumnValue>();
if (connIdValue == null) {
columns.add(new ColumnValue(mainColumn.dslPath(table), null));
for (var additionalColumn : additionalColumns) {
columns.add(new ColumnValue(additionalColumn.dslPath(table), null));
}
return columns;
}

var parts = connIdValue.toString().split(
Pattern.quote(delimiter), additionalColumns.size() + 1);
if (parts.length != additionalColumns.size() + 1) {
throw new IllegalArgumentException(
"UID has wrong number of parts: expected " + (additionalColumns.size() + 1)
+ ", got " + parts.length);
}

columns.add(new ColumnValue(mainColumn.dslPath(table), mainColumn.toSqlValue(parts[0])));
for (int i = 0; i < additionalColumns.size(); i++) {
var additionalColumn = additionalColumns.get(i);
columns.add(new ColumnValue(
additionalColumn.dslPath(table), additionalColumn.toSqlValue(parts[i + 1])));
}
return columns;
}

public Object toSqlValue(Object value) { return mainColumn.toSqlValue(value); }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ public SqlHandlerBuilder register(ObjectClass objectClass, Class<?> operationTyp
return this;
}

/** Registers a built-in handler without replacing an explicitly configured one. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After rebase on latest changes, the operations should be build using sepearate (per operation) SqlObject*BuilderImpl via SqlObjectOperationBuilderImpl
Registration is handled automatically the builder

public SqlHandlerBuilder registerIfAbsent(
ObjectClass objectClass, Class<?> operationType, Object handler) {
handlers.computeIfAbsent(objectClass, k -> new HashMap<>()).putIfAbsent(operationType, handler);
return this;
}

/**
* Evaluates a Groovy script from a classpath resource as handler definitions.
* Scripts can call objectClass("name") { search(...) } to register handlers.
Expand Down Expand Up @@ -220,4 +227,4 @@ public GroovyHandlerFacade sync(Map<String, Object> config) {
return this;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,16 @@ public SqlSchemaDetector(SqlBaseContext context) throws SQLException {

try (var wrapper = context.getConnection()) {
var meta = wrapper.getConnection().getMetaData();
var templatesFromRegistry = new SQLTemplatesRegistry().getTemplates(meta);
if (templatesFromRegistry == null) {
templatesFromRegistry = SQLTemplates.DEFAULT;
}

// For H2, use H2Templates with no quoting - unqualified column paths avoid table.column issues
var productName = meta.getDatabaseProductName();
SQLTemplates templatesFromRegistry;
if (productName != null && productName.toUpperCase().contains("H2")) {
templatesFromRegistry = new H2Templates(false);
} else {
var templatesBuilder = new SQLTemplatesRegistry().getBuilder(meta);
templatesFromRegistry = templatesBuilder != null
? templatesBuilder.printSchema().quote().build()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious, why is this needed?

: SQLTemplates.DEFAULT;
}
templates = templatesFromRegistry;
querydslConfig = new Configuration(templates);
Expand Down Expand Up @@ -260,10 +261,13 @@ private List<SqlColumnMeta> getColumnMetas(Connection conn, Table table) throws
var rawAutoInc = resolveColumn(colsRs, meta, "IS_AUTOINCREMENT");

var colLower = colName.toLowerCase();
var mappedColumnName = mappedColumnName(conn.getMetaData(), colName);
boolean isPk = pkList.contains(colLower);

// Use QueryDSL for Java type resolution (dialect-aware)
var javaType = resolveJavaType(dataType, typeName, columnSize, decimalDigits, table.table(), colLower);
var javaType = resolveJavaType(
dataType, typeName, columnSize, decimalDigits,
table.table(), mappedColumnName);

// Normalize type name using driver-typical TYPE_NAME (with fallback normalization)
var normalizedTypeName = normalizeTypeName(typeName);
Expand All @@ -272,7 +276,7 @@ private List<SqlColumnMeta> getColumnMetas(Connection conn, Table table) throws
var valueMapping = resolveValueMapping(javaType, dataType);

cols.add(SqlColumnMeta.builder()
.name(colLower)
.name(mappedColumnName)
.typeName(normalizedTypeName)
.typeCode(dataType)
.size(columnSize)
Expand Down Expand Up @@ -310,6 +314,22 @@ private List<SqlColumnMeta> getColumnMetas(Connection conn, Table table) throws
return cols;
}

/**
* Keeps explicitly quoted, case-sensitive column names while retaining the connector's
* historical lowercase names for ordinary unquoted identifiers.
*/
private String mappedColumnName(DatabaseMetaData metadata, String columnName) throws SQLException {
if (metadata.storesUpperCaseIdentifiers()
&& columnName.equals(columnName.toUpperCase(Locale.ROOT))) {
return columnName.toLowerCase(Locale.ROOT);
}
if (metadata.storesLowerCaseIdentifiers()
&& columnName.equals(columnName.toLowerCase(Locale.ROOT))) {
return columnName;
}
return columnName;
}

/**
* Collects column names that have unique constraints by scanning index metadata.
*/
Expand Down Expand Up @@ -471,4 +491,4 @@ public void setTableFilter(TableFilter tableFilter) {
}

record Table(String schema, String table, String tableType, String catalog) {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2026 Evolveum and contributors
*
* This work is licensed under European Union Public License v1.2. See LICENSE file for details.
*
*/
package com.evolveum.polygon.sql.base.write;

import com.evolveum.polygon.conndev.spi.ObjectCreateOperation;
import com.evolveum.polygon.sql.base.SqlBaseContext;
import com.evolveum.polygon.sql.base.build.api.SqlAttributeMapping;
import com.evolveum.polygon.sql.base.build.api.SqlObjectClassDefinition;
import com.querydsl.core.types.Path;
import com.querydsl.sql.dml.SQLInsertClause;
import org.identityconnectors.framework.common.exceptions.ConnectorException;
import org.identityconnectors.framework.common.objects.Attribute;
import org.identityconnectors.framework.common.objects.ConnectorObject;
import org.identityconnectors.framework.common.objects.OperationOptions;

import java.util.Set;

/** QueryDSL-based create operation for a writable SQL table. */
public class SqlCreateOperation extends SqlWriteOperationSupport implements ObjectCreateOperation {

public SqlCreateOperation(SqlBaseContext context, SqlObjectClassDefinition objectClass) {
super(context, objectClass);
}

@Override
public ConnectorObject create(Set<Attribute> createAttributes, OperationOptions options) {
requireWritable();
return inTransaction("Create " + objectClass.name(), connection -> {
var table = tablePath();
var uidDefinition = uidDefinition();
var suppliedUid = suppliedUid(createAttributes);
var assignments = createAssignments(table, createAttributes);
var insert = new SQLInsertClause(
connection.getConnection(), context.getSqlTemplates(), table);
setAssignments(insert, assignments);

final org.identityconnectors.framework.common.objects.Uid uid;
if (suppliedUid != null) {
var affected = insert.execute();
if (affected != 1) {
throw new ConnectorException(
"Create affected " + affected + " rows instead of one");
}
uid = suppliedUid;
} else {
if (uidDefinition.connId().isCreateable()) {
throw invalid("Required attribute " + uidDefinition.connId().getName() + " is missing");
}
var generatedPath = generatedKeyPath(uidDefinition.sql(), table);
uid = generatedUid(
uidDefinition.sql(), generatedKey(insert, generatedPath), table, assignments);
}

var created = findByUid(connection, uid, options, false);
if (created == null) {
throw new ConnectorException("Created object " + uid + " could not be read back");
}
return created;
});
}

private Path<?> generatedKeyPath(SqlAttributeMapping mapping, Path<?> table) {
if (mapping instanceof SqlAttributeMapping.SingleColumn singleColumn) {
return singleColumn.dslPath(table);
}
if (mapping instanceof SqlAttributeMapping.MultiColumn multiColumn) {
return multiColumn.mainColumn().dslPath(table);
}
throw new ConnectorException("Unsupported UID mapping " + mapping.getClass().getName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2026 Evolveum and contributors
*
* This work is licensed under European Union Public License v1.2. See LICENSE file for details.
*
*/
package com.evolveum.polygon.sql.base.write;

import com.evolveum.polygon.conndev.spi.ObjectDeleteOperation;
import com.evolveum.polygon.sql.base.SqlBaseContext;
import com.evolveum.polygon.sql.base.build.api.SqlObjectClassDefinition;
import com.querydsl.sql.dml.SQLDeleteClause;
import org.identityconnectors.framework.common.exceptions.ConnectorException;
import org.identityconnectors.framework.common.exceptions.UnknownUidException;
import org.identityconnectors.framework.common.objects.OperationOptions;
import org.identityconnectors.framework.common.objects.Uid;

/** QueryDSL-based delete operation for a writable SQL table. */
public class SqlDeleteOperation extends SqlWriteOperationSupport implements ObjectDeleteOperation {

public SqlDeleteOperation(SqlBaseContext context, SqlObjectClassDefinition objectClass) {
super(context, objectClass);
}

@Override
public void delete(Uid uid, OperationOptions options) {
requireWritable();
inTransaction("Delete " + objectClass.name(), connection -> {
var table = tablePath();
var delete = new SQLDeleteClause(
connection.getConnection(), context.getSqlTemplates(), table);
var affected = delete.where(uidPredicate(table, uid)).execute();
if (affected == 0) {
throw new UnknownUidException(uid, objectClass.objectClass());
}
if (affected != 1) {
throw new ConnectorException(
"Delete affected " + affected + " rows instead of one");
}
return null;
});
}
}
Loading