Skip to content
Merged
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 @@ -48,7 +48,7 @@ public FeatureMutationsSql(
}

public Reactive.Transformer<FeatureDataSql, String> getCreatorFlow(
SqlQueryMapping schema, Object executionContext, EpsgCrs crs) {
SqlQueryMapping schema, Object executionContext, Optional<String> id, EpsgCrs crs) {

RowCursor rowCursor = new RowCursor(schema.getMainTable().getFullPath());

Expand All @@ -57,7 +57,7 @@ public Reactive.Transformer<FeatureDataSql, String> getCreatorFlow(
return sqlClient
.get()
.getMutationFlow(
feature -> createInstanceInserts(feature, rowCursor, Optional.empty(), crs),
feature -> createInstanceInserts(feature, rowCursor, id, crs, false),
executionContext,
primaryKey,
Optional.empty());
Expand All @@ -73,7 +73,7 @@ public Reactive.Transformer<FeatureDataSql, String> getUpdaterFlow(
return sqlClient
.get()
.getMutationFlow(
feature -> createInstanceInserts(feature, rowCursor, Optional.of(id), crs),
feature -> createInstanceInserts(feature, rowCursor, Optional.of(id), crs, true),
executionContext,
primaryKey,
Optional.of(id));
Expand All @@ -89,12 +89,15 @@ public Reactive.Source<String> getDeletionSource(SqlQueryMapping mapping, String
}

List<Supplier<Tuple<String, Consumer<String>>>> createInstanceInserts(
FeatureDataSql feature, RowCursor rowCursor, Optional<String> id, EpsgCrs crs) {
boolean withId = id.isPresent();
FeatureDataSql feature,
RowCursor rowCursor,
Optional<String> id,
EpsgCrs crs,
boolean deleteFirst) {
SqlQuerySchema mainTable = feature.getMapping().getMainTable();

Stream<Supplier<Tuple<String, Consumer<String>>>> instance =
withId
deleteFirst
? Stream.concat(
Stream.of(createInstanceDelete(feature.getMapping(), id.get())),
createObjectInserts(feature, mainTable, rowCursor, id, crs).stream())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ public Supplier<Tuple<String, Consumer<String>>> createInsert(
.filter(
col ->
!Objects.equals(col.getName(), primaryKey)
|| schema.getStaticInserts().containsKey(primaryKey))
|| schema.getStaticInserts().containsKey(primaryKey)
|| col.hasOperation(Operation.DO_NOT_GENERATE))
// TODO: in deriver
.filter(col -> !col.hasOperation(Operation.CONSTANT))
.map(SqlQueryColumn::getName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,12 @@ private void addToMapping(
derive(schema, tableRule, columnRules, filterColumnRules, writableColumnRules, previous);

schemas.add(querySchema);
previous.add(pathParser.parseTablePath(tableRule.getSource()).getFullPath());
mapping.addTables(querySchema);

if (!columnRules.isEmpty() || !writableColumnRules.isEmpty()) {
previous.add(pathParser.parseTablePath(tableRule.getSource()).getFullPath());
}

if (tableRule.isWritable()
&& !seenWritableProperties.contains(tableRule.getTarget())
&& !Objects.equals(tableRule.getTarget(), ROOT_TARGET)) {
Expand Down Expand Up @@ -490,6 +493,10 @@ private Map<SqlQueryColumn.Operation, String[]> getColumnOperations(
SqlQueryColumn.Operation.CONSTANT, new String[] {sqlPath.getConstantValue().get()});
}

if (sqlPath.getGenerated().isPresent() && sqlPath.getGenerated().get() == false) {
operations.put(SqlQueryColumn.Operation.DO_NOT_GENERATE, new String[] {});
}

if (sqlPath.isConnected()) {
String connector = sqlPath.getConnector().orElse("");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
import de.ii.xtraplatform.features.sql.app.SqlQueryTemplates;
import de.ii.xtraplatform.features.sql.app.SqlQueryTemplatesDeriver;
import de.ii.xtraplatform.features.sql.domain.FeatureProviderSqlData.QueryGeneratorSettings;
import de.ii.xtraplatform.features.sql.domain.SqlQueryColumn.Operation;
import de.ii.xtraplatform.features.sql.infra.db.SourceSchemaValidatorSql;
import de.ii.xtraplatform.services.domain.Scheduler;
import de.ii.xtraplatform.streams.domain.Reactive;
Expand Down Expand Up @@ -171,6 +172,9 @@
* <p>### Dataset Changes
* <p>{@docVar:datasetChanges}
* <p>{@docTable:datasetChanges}
* <p>### Query Generation
* <p>Options for query generation.
* <p>{@docTable:queryGeneration}
* <p>### Source Path Defaults
* <p>Defaults for the path expressions in `sourcePath`, also see [Source Path
* Syntax](#path-syntax).
Expand Down Expand Up @@ -201,13 +205,54 @@
* <p>Additional columns for CRUD inserts can be set by adding `{inserts=columnName=value}`
* after the table name. The value may be a constant or a function, for example
* `{inserts=id=gen_random_uuid()&featuretype='type1'}`.
* <p>Also regarding CRUD, if a property with `role: ID` does match the primary key of the main
* table, but it should not be automatically generated on insert, add `{generated=false}` to the
* `sourcePath` of that property.
* <p>All table and column names must be unquoted identifiers.
* <p>Arbitrary SQL expressions for values are supported, for example to apply function calls.
* As an example, this `[EXPRESSION]{sql=$T$.length+55.5}` (instead of just `length`) would add
* a fixed amount to the length column. The prefix `$T$` will be replaced with the table alias.
* <p>### Query Generation
* <p>Options for query generation.
* <p>{@docTable:queryGeneration}
* <p>### CRUD
* <p>If the features of this provider should be mutated via the API, `datasetChanges.mode` has
* to be set to `CRUD`. Also consider the following paragraphs to understand which adjustments
* to the mapping might be necessary for your use case.
* <p>#### Table types
* <p><code>
* - `main` Main tables represent the instances of a feature type. They are defined in the
* upper-most `sourcePath` of a type.
* - `secondary` Secondary tables are connected to main tables via joins, either directly or
* through a chain of secondary and/or junction tables.
* - `junction` Junction tables usually represent m:n relations. They mainly contain two foreign keys
* of main and/or secondary tables.
* </code>
* <p>#### Primary keys
* <p>It is expected that every main and secondary table has a primary key, and that the value
* for the primary key is automatically generated on insert. Usually
* `sourcePathDefaults.primaryKey` should match the primary key name from the database. If not
* every table has the same primary key, it can be overridden, see [Source Path
* Syntax](#source-path-syntax).
* <p>In rare cases, when another column than the primary key is used in join conditions, the
* `primaryKey` in the mapping needs to match that column and differ from the actual primary
* key. An example would be a junction table that connects feature instances by id.
* <p>#### Feature ids
* <p>There are four variants for creating new feature ids on insert:
* <p><code>
* - **Primary key, auto-generated** The `sourcePath` of the property with `role: ID` matches
* the `primaryKey` and new values are automatically generated on insert by the database.
* - **Primary key, user-defined** The `sourcePath` of the property with `role: ID` matches the
* `primaryKey` and has the flag `{generated=false}`, the value from the payload given by the
* user is inserted.
* - **Other column, auto-generated** The `sourcePath` of the property with `role: ID` does not
* match the `primaryKey`, and the `sourcePath` of the main table has a matching `inserts` flag
* with a generation function, e.g. `{inserts=id=gen_random_uuid()'}` (see [Source Path Syntax](#source-path-syntax)).
* - **Other column, user-defined** The `sourcePath` of the property with `role: ID` does not match
* the `primaryKey`, and the `sourcePath` of the main table does not have a matching `inserts`
* flag, the value from the payload given by the user is inserted.
* </code>
* <p>#### Additional columns
* <p>If an insert or update should set additional columns that are not mapped to any property,
* use the `inserts` flag in `sourcePath` (see [Source Path Syntax](#source-path-syntax)), e.g.
* `{inserts=id=gen_random_uuid()&featuretype='Street'}`.
* @cfgPropertiesAdditionalDe ### Connection Info
* <p>Das Connection-Info-Objekt für SQL-Datenbanken wird wie folgt beschrieben:
* <p>{@docTable:connectionInfo}
Expand All @@ -217,6 +262,9 @@
* <p>### Datensatzänderungen
* <p>{@docVar:datasetChanges}
* <p>{@docTable:datasetChanges}
* <p>### Query-Generierung
* <p>Optionen für die Query-Generierung in `queryGeneration`.
* <p>{@docTable:queryGeneration}
* <p>### SQL-Pfad-Defaults
* <p>Defaults für die Pfad-Ausdrücke in `sourcePath`, siehe auch
* [SQL-Pfad-Syntax](#path-syntax).
Expand Down Expand Up @@ -253,14 +301,60 @@
* <p>Zusätzliche Spalten für CRUD Inserts können durch den Zusatz `{inserts=Spaltenname=Wert}`
* nach dem Tabellennamen angegeben werden. Der Wert kann eine Konstante oder eine Funktion
* sein, z.B. `{inserts=id=gen_random_uuid()&featuretype='type1'}`.
* <p>Auch im Hinblick auf CRUD: wenn eine Eigenschaft mit `role: ID` dem Primärschlüssel der
* Haupttabelle entspricht, aber der Wert beim Einfügen nicht automatisch generiert werden soll,
* dann ist für diese Eigenschaft in `sourcePath` das Flag `{generated=false}` anzugeben.
* <p>Alle Tabellen- und Spaltennamen müssen "unquoted Identifier" sein.
* <p>Beliebige SQL-Ausdrücke für Werte sind möglich, z.B. um Funktionsaufrufe anzuwenden. So
* würde z.B. dieser Ausdruck `[EXPRESSION]{sql=$T$.length+55.5}` (anstatt nur `length`) einen
* fixen Wert zur Längen-Spalte addieren. Der Präfix `$T$` wird durch den Tabellen-Alias
* ersetzt.
* <p>### Query-Generierung
* <p>Optionen für die Query-Generierung in `queryGeneration`.
* <p>{@docTable:queryGeneration}
* <p>### CRUD
* <p>Wenn die Features dieses Providers über die API verändert werden sollen, muss
* `datasetChanges.mode` auf `CRUD` gesetzt werden. Außerdem sind die folgenden Absätze zu
* beachten, um zu verstehen, welche Anpassungen am Mapping für den eigenen Anwendungsfall
* notwendig sind.
* <p>#### Tabellentypen
* <p><code>
* - `main` Haupttabellen repräsentieren die Instanzen einer Objektart. Sie sind im
* obersten `sourcePath` einer Objektart definiert.
* - `secondary` Sekundärtabellen sind über Joins mit Haupttabellen verbunden, entweder direkt
* oder über eine Kette von Sekundär- und/oder Junction-Tabellen.
* - `junction` Junction-Tabellen repräsentieren in der Regel m:n-Beziehungen. Sie enthalten
* hauptsächlich zwei Fremdschlüssel von Haupt- und/oder Sekundärtabellen.
* </code>
* <p>#### Primärschlüssel
* <p>Es wird erwartet, dass jede Haupt- und Sekundärtabelle einen Primärschlüssel hat und dass
* der Wert für den Primärschlüssel beim Einfügen automatisch generiert wird. In der Regel
* sollte `sourcePathDefaults.primaryKey` dem Primärschlüssel aus der Datenbank entsprechen.
* Wenn nicht jede Tabelle denselben Primärschlüssel hat, kann dieser überschrieben werden,
* siehe [SQL-Pfad-Syntax](#sql-pfad-syntax).
* <p>In seltenen Fällen, wenn in Join-Bedingungen eine andere Spalte als der Primärschlüssel
* verwendet wird, muss der `primaryKey` im Mapping dieser Spalte entsprechen und sich vom
* tatsächlichen Primärschlüssel unterscheiden. Ein Beispiel wäre eine Junction-Tabelle, die
* Feature-Instanzen über deren Ids verbindet.
* <p>#### Feature-Ids
* <p>Es gibt vier Varianten, um bei Inserts neue Feature-Ids zu erzeugen:
* <p><code>
* - **Primärschlüssel, automatisch generiert** Der `sourcePath` der Eigenschaft mit `role: ID`
* entspricht dem `primaryKey` und neue Werte werden beim Einfügen automatisch von der
* Datenbank generiert.
* - **Primärschlüssel, benutzerdefiniert** Der `sourcePath` der Eigenschaft mit `role: ID`
* entspricht dem `primaryKey` und hat das Flag `{generated=false}`, der Wert aus dem
* Payload wird eingefügt.
* - **Andere Spalte, automatisch generiert** Der `sourcePath` der Eigenschaft mit `role: ID`
* entspricht nicht dem `primaryKey`, und der `sourcePath` der Haupttabelle hat ein
* entsprechendes `inserts`-Flag mit einer Generierungsfunktion, z.B.
* `{inserts=id=gen_random_uuid()'}` (siehe [SQL-Pfad-Syntax](#sql-pfad-syntax)).
* - **Andere Spalte, benutzerdefiniert** Der `sourcePath` der Eigenschaft mit `role: ID`
* entspricht nicht dem `primaryKey`, und der `sourcePath` der Haupttabelle hat kein
* entsprechendes `inserts`-Flag, der Wert aus der Nutzlast wird eingefügt.
* </code>
* <p>#### Zusätzliche Spalten
* <p>Wenn bei einem Insert oder Update zusätzliche Spalten gesetzt werden sollen, die nicht auf
* eine Eigenschaft abgebildet sind, kann das `inserts`-Flag in `sourcePath` verwendet werden
* (siehe [SQL-Pfad-Syntax](#sql-pfad-syntax)), z.B.
* `{inserts=id=gen_random_uuid()&featuretype='Strasse'}`.
* @ref:cfgProperties {@link de.ii.xtraplatform.features.sql.domain.ImmutableFeatureProviderSqlData}
* @ref:connectionInfo {@link de.ii.xtraplatform.features.sql.domain.ImmutableConnectionInfoSql}
* @ref:pool {@link de.ii.xtraplatform.features.sql.domain.ImmutablePoolSettings}
Expand Down Expand Up @@ -1103,9 +1197,12 @@ private Map<String, List<SqlQueryMapping>> generateSqlQueryMappings() {

@Override
public MutationResult createFeatures(
String featureType, FeatureTokenSource featureTokenSource, EpsgCrs crs) {
String featureType,
FeatureTokenSource featureTokenSource,
EpsgCrs crs,
Optional<String> featureId) {

return writeFeatures(featureType, featureTokenSource, Optional.empty(), crs, false);
return writeFeatures(Type.CREATE, featureType, featureTokenSource, featureId, crs, false);
}

@Override
Expand All @@ -1115,7 +1212,13 @@ public MutationResult updateFeature(
FeatureTokenSource featureTokenSource,
EpsgCrs crs,
boolean partial) {
return writeFeatures(featureType, featureTokenSource, Optional.of(featureId), crs, partial);
return writeFeatures(
partial ? Type.UPDATE : Type.REPLACE,
featureType,
featureTokenSource,
Optional.of(featureId),
crs,
partial);
}

@Override
Expand Down Expand Up @@ -1143,7 +1246,38 @@ public MutationResult deleteFeature(String featureType, String id) {
return deletionStream.run().toCompletableFuture().join();
}

@Override
public boolean hasGeneratedId(String featureType) {
Optional<List<SqlQueryMapping>> queryMapping =
Optional.ofNullable(queryMappings.get(featureType));

if (queryMapping.isPresent()) {
return queryMapping.get().stream()
.allMatch(
mapping -> {
if (mapping.getColumnForId().isPresent() && mapping.getSchemaForId().isPresent()) {
String primaryKey = mapping.getColumnForId().get().first().getPrimaryKey();
String idColumn = mapping.getColumnForId().get().second().getName();

if (!Objects.equals(primaryKey, idColumn)) {
return false;
}

return !mapping
.getColumnForId()
.get()
.second()
.hasOperation(Operation.DO_NOT_GENERATE);
}
return true;
});
}

return true;
}

private MutationResult writeFeatures(
Type type,
String featureType,
FeatureTokenSource featureTokenSource,
Optional<String> featureId,
Expand All @@ -1158,15 +1292,13 @@ private MutationResult writeFeatures(
}

Transformer<FeatureDataSql, String> featureWriter =
featureId.isPresent()
? featureMutationsSql.getUpdaterFlow(
queryMapping.get().get(0), null, featureId.get(), crs)
: featureMutationsSql.getCreatorFlow(queryMapping.get().get(0), null, crs);
type == Type.CREATE
? featureMutationsSql.getCreatorFlow(queryMapping.get().get(0), null, featureId, crs)
: featureMutationsSql.getUpdaterFlow(
queryMapping.get().get(0), null, featureId.get(), crs);

ImmutableMutationResult.Builder builder =
ImmutableMutationResult.builder()
.type(featureId.isPresent() ? partial ? Type.UPDATE : Type.REPLACE : Type.CREATE)
.hasFeatures(false);
ImmutableMutationResult.builder().type(type).hasFeatures(false);

Source<FeatureDataSql> featureSqlSource =
featureTokenSource
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ default boolean getSortKeyUnique() {

Map<String, String> getStaticInserts();

Optional<Boolean> getGenerated();

// TODO: not needed any more? should be based on primary key detection
boolean getJunction();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ public interface SqlPathDefaults {

/**
* @langEn The default column that is used for join analysis if no differing primary key is set in
* the [sourcePath](#path-syntax). Only relevant for CRUD.
* the [sourcePath](#source-path-syntax). Only relevant for CRUD.
* @langDe Die Standard-Spalte die zur Analyse von Joins verwendet wird, wenn keine abweichende
* Spalte in `sourcePath` gesetzt wird. Nur relevant für CRUD.
* Spalte in [sourcePath](#sql-pfad-syntax) gesetzt wird. Nur relevant für CRUD.
* @default `id`
*/
@Value.Default
Expand All @@ -35,10 +35,10 @@ default String getPrimaryKey() {

/**
* @langEn The default column that is used to sort rows if no differing sort key is set in the
* [sourcePath](#path-syntax).
* @langDe Die Standard-Spalte die zur Sortierung von Reihen verwendet wird, wenn keine
* abweichende Spalte in `sourcePath` gesetzt wird. Es wird empfohlen, dass als Datentyp eine
* Ganzzahl verwendet wird.
* [sourcePath](#source-path-syntax).
* @langDe Die Standard-Spalte die zur Sortierung von Rows verwendet wird, wenn keine abweichende
* Spalte in [sourcePath](#sql-pfad-syntax) gesetzt wird. Es wird empfohlen, dass als Datentyp
* eine Ganzzahl verwendet wird.
* @default `id`
*/
@Value.Default
Expand All @@ -49,8 +49,9 @@ default String getSortKey() {
/**
* @since v3.3
* @langEn The default schema that is applied to tables without prefix in
* [sourcePaths](#path-syntax).
* @langDe Das Standard-Schema das Tabellen ohne Präfix in `sourcePath` vorangestellt wird.
* [sourcePaths](#source-path-syntax).
* @langDe Das Standard-Schema das Tabellen ohne Präfix in [sourcePaths](#sql-pfad-syntax)
* vorangestellt wird.
* @default null
*/
Optional<String> getSchema();
Expand Down
Loading