From 8c8b5f8de6c643102e3bf29294d5ed7007cf5dbd Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Thu, 16 Apr 2020 13:29:15 +0200 Subject: [PATCH] [jOOQ/jOOQ#8950] Emulate JSON_TABLE in PostgreSQL --- .../JSONTableColumnForOrdinalityStep.java | 3 +- .../org/jooq/JSONTableColumnPathStep.java | 3 +- .../org/jooq/JSONTableColumnsFirstStep.java | 13 +- jOOQ/src/main/java/org/jooq/impl/DSL.java | 8 +- .../main/java/org/jooq/impl/JSONTable.java | 297 ++++++++++-------- 5 files changed, 185 insertions(+), 139 deletions(-) diff --git a/jOOQ/src/main/java/org/jooq/JSONTableColumnForOrdinalityStep.java b/jOOQ/src/main/java/org/jooq/JSONTableColumnForOrdinalityStep.java index 22d4e98910..b95fc9b989 100644 --- a/jOOQ/src/main/java/org/jooq/JSONTableColumnForOrdinalityStep.java +++ b/jOOQ/src/main/java/org/jooq/JSONTableColumnForOrdinalityStep.java @@ -39,6 +39,7 @@ package org.jooq; import static org.jooq.SQLDialect.MYSQL; // ... +import static org.jooq.SQLDialect.POSTGRES; /** * A step in the construction of an JSON_TABLE expression. @@ -51,7 +52,7 @@ public interface JSONTableColumnForOrdinalityStep { * Specify the FOR ORDINALITY clause on a column in the * COLUMNS clause of the JSON_TABLE predicate. */ - @Support({ MYSQL }) + @Support({ MYSQL, POSTGRES }) JSONTableColumnsStep forOrdinality(); } diff --git a/jOOQ/src/main/java/org/jooq/JSONTableColumnPathStep.java b/jOOQ/src/main/java/org/jooq/JSONTableColumnPathStep.java index f389b233d3..7c8631f39d 100644 --- a/jOOQ/src/main/java/org/jooq/JSONTableColumnPathStep.java +++ b/jOOQ/src/main/java/org/jooq/JSONTableColumnPathStep.java @@ -40,6 +40,7 @@ package org.jooq; // ... import static org.jooq.SQLDialect.MYSQL; // ... +import static org.jooq.SQLDialect.POSTGRES; /** * A step in the construction of an JSON_TABLE expression. @@ -52,7 +53,7 @@ public interface JSONTableColumnPathStep extends JSONTableColumnForOrdinalitySte * Specify the PATH of a column in the COLUMNS * clause of the JSON_TABLE predicate. */ - @Support({ MYSQL }) + @Support({ MYSQL, POSTGRES }) JSONTableColumnsStep path(String path); } diff --git a/jOOQ/src/main/java/org/jooq/JSONTableColumnsFirstStep.java b/jOOQ/src/main/java/org/jooq/JSONTableColumnsFirstStep.java index 3c80e7f2a6..28cccff58d 100644 --- a/jOOQ/src/main/java/org/jooq/JSONTableColumnsFirstStep.java +++ b/jOOQ/src/main/java/org/jooq/JSONTableColumnsFirstStep.java @@ -40,6 +40,7 @@ package org.jooq; // ... import static org.jooq.SQLDialect.MYSQL; // ... +import static org.jooq.SQLDialect.POSTGRES; /** * A step in the construction of an JSON_TABLE expression. @@ -52,42 +53,42 @@ public interface JSONTableColumnsFirstStep { * Add a column to the COLUMNS clause of the * JSON_TABLE expression. */ - @Support({ MYSQL }) + @Support({ MYSQL, POSTGRES }) JSONTableColumnForOrdinalityStep column(String name); /** * Add a column to the COLUMNS clause of the * JSON_TABLE expression. */ - @Support({ MYSQL }) + @Support({ MYSQL, POSTGRES }) JSONTableColumnForOrdinalityStep column(Name name); /** * Add a column to the COLUMNS clause of the * JSON_TABLE expression. */ - @Support({ MYSQL }) + @Support({ MYSQL, POSTGRES }) JSONTableColumnPathStep column(Field name); /** * Add a column to the COLUMNS clause of the * JSON_TABLE expression. */ - @Support({ MYSQL }) + @Support({ MYSQL, POSTGRES }) JSONTableColumnPathStep column(String name, DataType type); /** * Add a column to the COLUMNS clause of the * JSON_TABLE expression. */ - @Support({ MYSQL }) + @Support({ MYSQL, POSTGRES }) JSONTableColumnPathStep column(Name name, DataType type); /** * Add a column to the COLUMNS clause of the * JSON_TABLE expression. */ - @Support({ MYSQL }) + @Support({ MYSQL, POSTGRES }) JSONTableColumnPathStep column(Field name, DataType type); } diff --git a/jOOQ/src/main/java/org/jooq/impl/DSL.java b/jOOQ/src/main/java/org/jooq/impl/DSL.java index ca046ad839..18052ed198 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DSL.java +++ b/jOOQ/src/main/java/org/jooq/impl/DSL.java @@ -18769,7 +18769,7 @@ public class DSL { /** * The JSON table function. */ - @Support({ MYSQL }) + @Support({ MYSQL, POSTGRES }) public static JSONTableColumnsFirstStep jsonTable(JSON json, String path) { return jsonTable(Tools.field(json), Tools.field(path)); } @@ -18777,7 +18777,7 @@ public class DSL { /** * The JSON table function. */ - @Support({ MYSQL }) + @Support({ MYSQL, POSTGRES }) public static JSONTableColumnsFirstStep jsonTable(Field json, Field path) { return new JSONTable(nullSafe(json), nullSafe(path)); } @@ -18785,7 +18785,7 @@ public class DSL { /** * The JSON table function. */ - @Support({ MYSQL }) + @Support({ MYSQL, POSTGRES }) public static JSONTableColumnsFirstStep jsonbTable(JSONB json, String path) { return jsonbTable(Tools.field(json), Tools.field(path)); } @@ -18793,7 +18793,7 @@ public class DSL { /** * The JSON table function. */ - @Support({ MYSQL }) + @Support({ MYSQL, POSTGRES }) public static JSONTableColumnsFirstStep jsonbTable(Field json, Field path) { return new JSONTable(nullSafe(json), nullSafe(path)); } diff --git a/jOOQ/src/main/java/org/jooq/impl/JSONTable.java b/jOOQ/src/main/java/org/jooq/impl/JSONTable.java index 0cff1d3873..02fcae36f0 100644 --- a/jOOQ/src/main/java/org/jooq/impl/JSONTable.java +++ b/jOOQ/src/main/java/org/jooq/impl/JSONTable.java @@ -42,6 +42,10 @@ import static org.jooq.SQLDialect.MYSQL; // ... import static org.jooq.conf.ParamType.INLINED; import static org.jooq.impl.DSL.inline; +import static org.jooq.impl.DSL.keyword; +import static org.jooq.impl.DSL.rowNumber; +import static org.jooq.impl.DSL.select; +import static org.jooq.impl.DSL.val; import static org.jooq.impl.Keywords.K_COLUMNS; import static org.jooq.impl.Keywords.K_ERROR; import static org.jooq.impl.Keywords.K_FOR; @@ -50,6 +54,7 @@ import static org.jooq.impl.Keywords.K_ON; import static org.jooq.impl.Keywords.K_ORDINALITY; import static org.jooq.impl.Keywords.K_PATH; import static org.jooq.impl.Names.N_JSON_TABLE; +import static org.jooq.impl.SQLDataType.JSONB; import java.util.ArrayList; import java.util.List; @@ -58,11 +63,13 @@ import java.util.Set; import org.jooq.Context; import org.jooq.DataType; import org.jooq.Field; +import org.jooq.JSONB; import org.jooq.JSONTableColumnPathStep; import org.jooq.Name; // ... import org.jooq.Record; import org.jooq.SQLDialect; +import org.jooq.SelectField; import org.jooq.Table; import org.jooq.TableOptions; import org.jooq.conf.ParamType; @@ -80,141 +87,177 @@ implements */ private static final long serialVersionUID = -4881363881968319258L; private static final Set REQUIRES_COLUMN_PATH = SQLDialect.supportedBy(MYSQL); - private static final Set REQUIRES_ALIASING = SQLDialect.supportedBy(MYSQL); - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ctx.formatIndentEnd() - .formatNewLine() - .sql(')'); + private final Field path; + private final Field json; + private final QueryPartList columns; + private transient Fields fields; + + JSONTable(Field json, Field path) { + this(json, path, null); + } + + private JSONTable( + Field json, + Field path, + QueryPartList columns + ) { + super(TableOptions.expression(), N_JSON_TABLE); + + this.json = json; + this.path = path; + this.columns = columns == null ? new QueryPartList<>() : columns; + } + + // ------------------------------------------------------------------------- + // XXX: DSL API + // ------------------------------------------------------------------------- + + @Override + public final JSONTable column(String name) { + return column(DSL.name(name)); + } + + @Override + public final JSONTable column(Name name) { + return column(DSL.field(name)); + } + + @Override + public final JSONTable column(Field name) { + return column(name, name.getDataType()); + } + + @Override + public final JSONTable column(String name, DataType type) { + return column(DSL.name(name), type); + } + + @Override + public final JSONTable column(Name name, DataType type) { + return column(DSL.field(name), type); + } + + @Override + public final JSONTable column(Field name, DataType type) { + QueryPartList c = new QueryPartList<>(columns); + c.add(new JSONTableColumn(name, type, false, null)); + return new JSONTable(json, path, c); + } + + @Override + public final JSONTable forOrdinality() { + return path0(true, null); + } + + @Override + public final JSONTable path(String p) { + return path0(false, p); + } + + private final JSONTable path0(boolean forOrdinality, String p) { + QueryPartList c = new QueryPartList<>(columns); + int i = c.size() - 1; + JSONTableColumn last = c.get(i); + c.set(i, new JSONTableColumn(last.field, last.type, forOrdinality, p)); + return new JSONTable(json, path, c); + } + + // ------------------------------------------------------------------------- + // XXX: Table API + // ------------------------------------------------------------------------- + + @Override + public final Class getRecordType() { + return RecordImplN.class; + } + + @Override + final Fields fields0() { + if (fields == null) { + List> f = new ArrayList<>(); + + for (JSONTableColumn c : columns) + f.add(c.field.getDataType() == c.type ? c.field : field(c.field.getQualifiedName(), c.type)); + + fields = new Fields<>(f); + } + + return fields; + } + + // ------------------------------------------------------------------------- + // XXX: QueryPart API + // ------------------------------------------------------------------------- + + @Override + public final void accept(Context ctx) { + switch (ctx.family()) { + case POSTGRES: + acceptPostgres(ctx); + break; + + default: + acceptStandard(ctx); + break; } } - private void acceptJSONPath(Context ctx) { + private final void acceptPostgres(Context ctx) { + List> cols = new ArrayList<>(); + + for (JSONTableColumn col : columns) + if (col.forOrdinality) + cols.add(rowNumber().over().as(col.field)); + else + cols.add( + DSL.field("(jsonb_path_query_first(j, {0}::jsonpath)->>0)::{1}", + col.path != null ? val(col.path) : inline("$." + col.field.getName()), + keyword(col.type.getCastTypeName(ctx.configuration())) + ).as(col.field) + ); + + ctx.sql('(') + .formatIndentStart() + .formatNewLine() + .subquery(true) + .visit( + select(cols).from("jsonb_path_query({0}, {1}::jsonpath) as t(j)", + json.getType() == JSONB.class ? json : json.cast(JSONB), + path + ) + ) + .subquery(false) + .formatIndentEnd() + .formatNewLine() + .sql(')'); + } + + private final void acceptStandard(Context ctx) { + ctx.visit(K_JSON_TABLE).sql('(') + .formatIndentStart() + .formatNewLine(); + + ctx.visit(json).sql(',').formatSeparator(); + acceptJSONPath(ctx); + + ctx.formatSeparator().visit(K_COLUMNS).sql(" (").visit(columns).sql(')'); + + + + + + + ctx.formatIndentEnd() + .formatNewLine() + .sql(')'); + } + + private final void acceptJSONPath(Context ctx) {