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
242 changes: 242 additions & 0 deletions base/src/main/java/com/evolveum/polygon/sql/base/SqlTableAccess.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
/*
* 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;

import com.evolveum.polygon.sql.base.connection.SqlConnection;
import com.evolveum.polygon.sql.base.schema.SqlColumnMeta;
import com.evolveum.polygon.sql.base.schema.SqlTableInfo;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.PathMetadataFactory;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.SimpleExpression;
import com.querydsl.sql.RelationalPathBase;
import com.querydsl.sql.dml.SQLDeleteClause;
import com.querydsl.sql.dml.SQLInsertClause;
import org.identityconnectors.framework.common.exceptions.ConnectorException;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;

/**
* Metadata-aware access to a dynamically detected SQL table.
*
* <p>Centralizes case-insensitive column lookup, typed QueryDSL paths, value conversion,
* schema-qualified DML, and predicates used by related-table readers and writers.</p>
*/
public final class SqlTableAccess {

private final SqlBaseContext context;
private final SqlTableInfo table;
private final RelationalPathBase<?> path;
private final Map<String, Path<?>> columnPaths = new LinkedHashMap<>();

public SqlTableAccess(SqlBaseContext context, String tableName, String alias) {
this(context, requireTable(context, tableName), alias);
}

public SqlTableAccess(
SqlBaseContext context, String tableName, RelationalPathBase<?> path) {
this.context = context;
this.table = requireTable(context, tableName);
this.path = path;
}

private SqlTableAccess(SqlBaseContext context, SqlTableInfo table, String alias) {
this.context = context;
this.table = table;
var schema = table.getSchema();
if (schema != null && (schema.isBlank() || "null".equalsIgnoreCase(schema))) {
schema = null;
}
this.path = new RelationalPathBase<>(
Object.class, PathMetadataFactory.forVariable(alias), schema, table.getName());
}

public SqlTableInfo metadata() {
return table;
}

public RelationalPathBase<?> path() {
return path;
}

public String actualColumn(String columnName) {
var column = column(columnName);
if (column == null) {
throw new ConnectorException(
"Column " + columnName + " was not detected in " + table.getName());
}
return column.getName();
}

public SqlColumnMeta column(String columnName) {
return table.getColumns().stream()
.filter(candidate -> candidate.getName().equalsIgnoreCase(columnName))
.findFirst()
.orElse(null);
}

public Path<?> columnPath(String columnName) {
var key = columnName.toLowerCase(Locale.ROOT);
var existing = columnPaths.get(key);
if (existing != null) {
return existing;
}
var column = column(columnName);
Path<?> result;
if (column != null && column.getValueMapping() != null) {
result = column.getValueMapping().pathFor(path, column.getName());
} else {
result = Expressions.path(Object.class, path, actualColumn(columnName));
}
columnPaths.put(key, result);
return result;
}

public Object value(Tuple row, String columnName) {
return row.get(columnPath(columnName));
}

public Object toWireValue(String columnName, Object value) {
var column = column(columnName);
if (column == null || column.getValueMapping() == null || value == null) {
return value;
}
var mapping = column.getValueMapping();
if (mapping.primaryWireType().isInstance(value)) {
return value;
}
if (value instanceof String stringValue) {
return parse(stringValue, mapping.primaryWireType());
}
return mapping.toWireValue(value);
}

public Object toConnIdValue(String columnName, Object value) {
var column = column(columnName);
if (value == null || column == null || column.getValueMapping() == null) {
return value;
}
var mapping = column.getValueMapping();
var wireValue = value;
if (!mapping.primaryWireType().isInstance(value) && value instanceof Number) {
wireValue = parse(value.toString(), mapping.primaryWireType());
}
return mapping.toConnIdValue(wireValue);
}

public BooleanExpression predicate(Map<String, Object> criteria) {
if (criteria.isEmpty()) {
throw new IllegalArgumentException("A SQL predicate requires at least one criterion");
}
BooleanExpression result = null;
for (var entry : criteria.entrySet()) {
var current = equal(columnPath(entry.getKey()), entry.getValue());
result = result == null ? current : result.and(current);
}
return result;
}

public BooleanExpression matchingAny(Collection<? extends Map<String, Object>> alternatives) {
if (alternatives.isEmpty()) {
throw new IllegalArgumentException("A SQL predicate requires at least one alternative");
}
BooleanExpression result = null;
for (var alternative : alternatives) {
var current = predicate(alternative);
result = result == null ? current : result.or(current);
}
return result;
}

public void insert(SqlConnection connection, Map<String, Object> assignments) {
if (assignments.isEmpty()) {
throw new IllegalArgumentException("No values supplied for table " + table.getName());
}
var insert = new SQLInsertClause(
connection.getConnection(), context.getSqlTemplates(), path);
assignments.forEach((column, value) -> set(insert, columnPath(column), value));
insert.execute();
}

public long delete(SqlConnection connection, Map<String, Object> criteria) {
return new SQLDeleteClause(connection.getConnection(), context.getSqlTemplates(), path)
.where(predicate(criteria))
.execute();
}

public boolean exists(SqlConnection connection, Map<String, Object> criteria) {
return connection.newQuery()
.select(Expressions.ONE)
.from(path)
.where(predicate(criteria))
.fetchFirst() != null;
}

private static SqlTableInfo requireTable(SqlBaseContext context, String tableName) {
var table = context.findTableInfo(tableName);
if (table == null) {
throw new ConnectorException("No detected metadata for related table " + tableName);
}
return table;
}

private static Object parse(String value, Class<?> targetType) {
if (targetType == String.class) {
return value;
}
if (targetType == BigInteger.class) {
return new BigInteger(value);
}
if (targetType == BigDecimal.class) {
return new BigDecimal(value);
}
if (targetType == Integer.class) {
return Integer.valueOf(value);
}
if (targetType == Long.class) {
return Long.valueOf(value);
}
if (targetType == Short.class) {
return Short.valueOf(value);
}
if (targetType == Byte.class) {
return Byte.valueOf(value);
}
if (targetType == Double.class) {
return Double.valueOf(value);
}
if (targetType == Float.class) {
return Float.valueOf(value);
}
if (targetType == Boolean.class) {
return Boolean.valueOf(value);
}
return value;
}

@SuppressWarnings({ "rawtypes", "unchecked" })
private static BooleanExpression equal(Path<?> path, Object value) {
var expression = (SimpleExpression) path;
return value == null ? expression.isNull() : expression.eq(value);
}

@SuppressWarnings({ "rawtypes", "unchecked" })
private static void set(SQLInsertClause insert, Path<?> path, Object value) {
if (value == null) {
insert.setNull((Path) path);
} else {
insert.set((Path) path, value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,36 @@

import com.evolveum.polygon.conndev.dev.ConnDevObjectClass;
import com.evolveum.polygon.conndev.schema.BaseObjectClassDefinition;
import com.evolveum.polygon.sql.base.schema.SqlChildJoinConfig;
import com.evolveum.polygon.sql.base.schema.SqlJunctionJoinConfig;
import org.identityconnectors.framework.common.objects.Attribute;
import org.identityconnectors.framework.common.objects.AttributeBuilder;
import org.identityconnectors.framework.common.objects.ObjectClassInfo;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;


public class SqlObjectClassDefinition extends BaseObjectClassDefinition<SqlAttributeDefinition> {

private final SqlSchemaBuilderImpl.SqlObjectClassMapping sql;
private final Boolean readOnly;
private final List<SqlChildJoinConfig> relatedAttributeJoinConfigs;
private final List<SqlJunctionJoinConfig> junctionJoinConfigs;

public SqlObjectClassDefinition(ObjectClassInfo connId,
Map<String, SqlAttributeDefinition> nativeAttrs,
Map<String, SqlAttributeDefinition> connIdAttrs,
SqlSchemaBuilderImpl.SqlObjectClassMapping sql,
Boolean readOnly) {
Boolean readOnly,
List<SqlChildJoinConfig> relatedAttributeJoinConfigs,
List<SqlJunctionJoinConfig> junctionJoinConfigs) {
super(connId, nativeAttrs, connIdAttrs);
this.sql = sql;
this.readOnly = readOnly;
this.relatedAttributeJoinConfigs = List.copyOf(relatedAttributeJoinConfigs);
this.junctionJoinConfigs = List.copyOf(junctionJoinConfigs);
}

/**
Expand All @@ -51,6 +60,16 @@ public SqlSchemaBuilderImpl.SqlObjectClassMapping sql() {
return this.sql;
}

/** Child-table joins exposed as scalar or embedded attributes on this object class. */
public List<SqlChildJoinConfig> relatedAttributeJoinConfigs() {
return relatedAttributeJoinConfigs;
}

/** Junction-table joins exposed as reference attributes on this object class. */
public List<SqlJunctionJoinConfig> junctionJoinConfigs() {
return junctionJoinConfigs;
}

@Override
public void contribute(ConnDevObjectClass target) {
if (sql == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class SqlObjectClassSchemaBuilderImpl extends BaseObjectClassDefinitionBu
private Boolean onlyExplicitlyListed = false;
private DefinitionValue<Boolean> readOnly = DefinitionValue.DEFAULT_FALSE;
private final Set<String> explicitRemoteNames = new LinkedHashSet<>();
private final List<SqlChildJoinConfig> embeddedJoinConfigs = new ArrayList<>();
private final List<SqlChildJoinConfig> relatedAttributeJoinConfigs = new ArrayList<>();
private final List<SqlJunctionJoinConfig> junctionJoinConfigs = new ArrayList<>();

public SqlObjectClassSchemaBuilderImpl(SqlSchemaBuilderImpl restSchemaBuilder, DefinitionValue<String> name) {
Expand Down Expand Up @@ -90,26 +90,16 @@ public Set<String> getExplicitRemoteNames() {
return explicitRemoteNames;
}

/** Adds a join config for an embedded child table. */
public void addEmbeddedJoinConfig(SqlChildJoinConfig config) {
embeddedJoinConfigs.add(config);
/** Adds a join config for a scalar or embedded attribute stored in a child table. */
public void addRelatedAttributeJoinConfig(SqlChildJoinConfig config) {
relatedAttributeJoinConfigs.add(config);
}

/** Adds a join config for a junction table reference. */
public void addJunctionJoinConfig(SqlJunctionJoinConfig config) {
junctionJoinConfigs.add(config);
}

/** Returns the embedded join configurations. */
public List<SqlChildJoinConfig> getEmbeddedJoinConfigs() {
return Collections.unmodifiableList(embeddedJoinConfigs);
}

/** Returns the junction join configurations. */
public List<SqlJunctionJoinConfig> getJunctionJoinConfigs() {
return Collections.unmodifiableList(junctionJoinConfigs);
}

/**
* Finds an attribute by name using case-insensitive matching.
* Used for Oracle compatibility where column names may differ in case.
Expand Down Expand Up @@ -201,7 +191,9 @@ protected SqlObjectClassDefinition buildImpl(ObjectClassInfo connIdInfo,

var sql = new SqlSchemaBuilderImpl.SqlObjectClassMapping(schema, table);

return new SqlObjectClassDefinition(connIdInfo, nativeAttrs, connIdAttrs, sql, readOnly.value());
return new SqlObjectClassDefinition(
connIdInfo, nativeAttrs, connIdAttrs, sql, readOnly.value(),
relatedAttributeJoinConfigs, junctionJoinConfigs);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,33 @@
*/
package com.evolveum.polygon.sql.base.schema;

import java.util.List;

import com.evolveum.polygon.sql.base.schema.ChildTableRelationship.JoinKey;

/**
* Configuration for an embedded child table join.
* Configuration for an attribute stored in an owned child table.
* For simple attribute joins (FK + one value column), {@code valueColumn} specifies
* the column to extract as a scalar value instead of building an EmbeddedObject.
*/
public record SqlChildJoinConfig(
String parentTable,
String childTable,
String parentJoinColumn,
String childJoinColumn,
List<JoinKey> joinKeys,
boolean multiValued,
String targetAttributeName,
String valueColumn
) {
public SqlChildJoinConfig(String childTable, String parentJoinColumn, String childJoinColumn,
boolean multiValued, String targetAttributeName) {
this(childTable, parentJoinColumn, childJoinColumn, multiValued, targetAttributeName, null);
public SqlChildJoinConfig {
joinKeys = List.copyOf(joinKeys);
if (joinKeys.isEmpty()) {
throw new IllegalArgumentException("A child-table relationship requires at least one join key");
}
}

public SqlChildJoinConfig(
String parentTable, String childTable, List<JoinKey> joinKeys,
boolean multiValued, String targetAttributeName) {
this(parentTable, childTable, joinKeys, multiValued, targetAttributeName, null);
}
}
Loading