-
Notifications
You must be signed in to change notification settings - Fork 0
Implement built-in SQL create, update and delete #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,6 +43,13 @@ public SqlHandlerBuilder register(ObjectClass objectClass, Class<?> operationTyp | |
| return this; | ||
| } | ||
|
|
||
| /** Registers a built-in handler without replacing an explicitly configured one. */ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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. | ||
|
|
@@ -220,4 +227,4 @@ public GroovyHandlerFacade sync(Map<String, Object> config) { | |
| return this; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
|
@@ -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); | ||
|
|
@@ -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) | ||
|
|
@@ -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. | ||
| */ | ||
|
|
@@ -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; | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
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