From 6d39b16e732b0efee8261dd7b3adb6622c8d1728 Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Wed, 2 Jul 2014 11:37:48 +0200 Subject: [PATCH] [#3375] Add support for PostgreSQL table-valued functions --- .../jooq/util/postgres/PostgresDatabase.java | 71 ++++++--- .../postgres/PostgresRoutineDefinition.java | 7 + .../postgres/PostgresTableValuedFunction.java | 143 ++++++++++++++++++ .../org/jooq/test/postgres/create.sql | 79 +++++++++- .../test/java/org/jooq/test/PostgresTest.java | 59 +++++++- .../main/java/org/jooq/impl/TableImpl.java | 10 +- 6 files changed, 337 insertions(+), 32 deletions(-) create mode 100644 jOOQ-meta/src/main/java/org/jooq/util/postgres/PostgresTableValuedFunction.java diff --git a/jOOQ-meta/src/main/java/org/jooq/util/postgres/PostgresDatabase.java b/jOOQ-meta/src/main/java/org/jooq/util/postgres/PostgresDatabase.java index 5e1c85bc7c..76fa538692 100644 --- a/jOOQ-meta/src/main/java/org/jooq/util/postgres/PostgresDatabase.java +++ b/jOOQ-meta/src/main/java/org/jooq/util/postgres/PostgresDatabase.java @@ -44,6 +44,7 @@ package org.jooq.util.postgres; import static org.jooq.impl.DSL.count; import static org.jooq.impl.DSL.decode; import static org.jooq.impl.DSL.field; +import static org.jooq.impl.DSL.inline; import static org.jooq.impl.DSL.max; import static org.jooq.impl.DSL.name; import static org.jooq.impl.DSL.select; @@ -64,6 +65,7 @@ import static org.jooq.util.postgres.pg_catalog.Tables.PG_CLASS; import static org.jooq.util.postgres.pg_catalog.Tables.PG_ENUM; import static org.jooq.util.postgres.pg_catalog.Tables.PG_INHERITS; import static org.jooq.util.postgres.pg_catalog.Tables.PG_NAMESPACE; +import static org.jooq.util.postgres.pg_catalog.Tables.PG_PROC; import static org.jooq.util.postgres.pg_catalog.Tables.PG_TYPE; import java.sql.SQLException; @@ -254,23 +256,46 @@ public class PostgresDatabase extends AbstractDatabase { Map map = new HashMap(); for (Record record : create() - .select( - TABLES.TABLE_SCHEMA, - TABLES.TABLE_NAME) - .from(TABLES) - .where(TABLES.TABLE_SCHEMA.in(getInputSchemata())) - .orderBy( - TABLES.TABLE_SCHEMA, - TABLES.TABLE_NAME) + .select() + .from( + select( + TABLES.TABLE_SCHEMA, + TABLES.TABLE_NAME, + TABLES.TABLE_NAME.as("specific_name"), + inline(false).as("table_valued_function")) + .from(TABLES) + .where(TABLES.TABLE_SCHEMA.in(getInputSchemata())) + + // [#3375] Include table-valued functions in the set of tables + .unionAll( + select( + ROUTINES.ROUTINE_SCHEMA, + ROUTINES.ROUTINE_NAME, + ROUTINES.SPECIFIC_NAME, + inline(true).as("table_valued_function")) + .from(ROUTINES) + .join(PG_NAMESPACE).on(ROUTINES.SPECIFIC_SCHEMA.eq(PG_NAMESPACE.NSPNAME)) + .join(PG_PROC).on(PG_PROC.PRONAMESPACE.eq(oid(PG_NAMESPACE))) + .and(PG_PROC.PRONAME.concat("_").concat(oid(PG_PROC)).eq(ROUTINES.SPECIFIC_NAME)) + .where(ROUTINES.ROUTINE_SCHEMA.in(getInputSchemata())) + .and(PG_PROC.PRORETSET)) + .asTable("tables")) + .orderBy(1, 2) .fetch()) { SchemaDefinition schema = getSchema(record.getValue(TABLES.TABLE_SCHEMA)); String name = record.getValue(TABLES.TABLE_NAME); + boolean tableValuedFunction = record.getValue("table_valued_function", boolean.class); String comment = ""; - PostgresTableDefinition t = new PostgresTableDefinition(schema, name, comment); - result.add(t); - map.put(name(schema.getName(), name), t); + if (tableValuedFunction) { + result.add(new PostgresTableValuedFunction(schema, name, record.getValue(ROUTINES.SPECIFIC_NAME), comment)); + } + else { + PostgresTableDefinition t = new PostgresTableDefinition(schema, name, comment); + result.add(t); + map.put(name(schema.getName(), name), t); + } } PgClass ct = PG_CLASS.as("ct"); @@ -470,9 +495,9 @@ public class PostgresDatabase extends AbstractDatabase { .when(DSL.exists( selectOne() .from(PARAMETERS) - .where(PARAMETERS.SPECIFIC_SCHEMA.equal(r1.SPECIFIC_SCHEMA)) - .and(PARAMETERS.SPECIFIC_NAME.equal(r1.SPECIFIC_NAME)) - .and(upper(PARAMETERS.PARAMETER_MODE).notEqual("IN"))), + .where(PARAMETERS.SPECIFIC_SCHEMA.eq(r1.SPECIFIC_SCHEMA)) + .and(PARAMETERS.SPECIFIC_NAME.eq(r1.SPECIFIC_NAME)) + .and(upper(PARAMETERS.PARAMETER_MODE).ne("IN"))), val("void")) .otherwise(r1.DATA_TYPE).as("data_type"), r1.CHARACTER_MAXIMUM_LENGTH, @@ -486,18 +511,24 @@ public class PostgresDatabase extends AbstractDatabase { selectOne() .from(r2) .where(r2.ROUTINE_SCHEMA.in(getInputSchemata())) - .and(r2.ROUTINE_SCHEMA.equal(r1.ROUTINE_SCHEMA)) - .and(r2.ROUTINE_NAME.equal(r1.ROUTINE_NAME)) - .and(r2.SPECIFIC_NAME.notEqual(r1.SPECIFIC_NAME))), + .and(r2.ROUTINE_SCHEMA.eq(r1.ROUTINE_SCHEMA)) + .and(r2.ROUTINE_NAME.eq(r1.ROUTINE_NAME)) + .and(r2.SPECIFIC_NAME.ne(r1.SPECIFIC_NAME))), select(count()) .from(r2) .where(r2.ROUTINE_SCHEMA.in(getInputSchemata())) - .and(r2.ROUTINE_SCHEMA.equal(r1.ROUTINE_SCHEMA)) - .and(r2.ROUTINE_NAME.equal(r1.ROUTINE_NAME)) - .and(r2.SPECIFIC_NAME.lessOrEqual(r1.SPECIFIC_NAME)).asField()) + .and(r2.ROUTINE_SCHEMA.eq(r1.ROUTINE_SCHEMA)) + .and(r2.ROUTINE_NAME.eq(r1.ROUTINE_NAME)) + .and(r2.SPECIFIC_NAME.le(r1.SPECIFIC_NAME)).asField()) .as("overload")) .from(r1) + + // [#3375] Exclude table-valued functions as they're already generated as tables + .join(PG_NAMESPACE).on(PG_NAMESPACE.NSPNAME.eq(r1.SPECIFIC_SCHEMA)) + .join(PG_PROC).on(PG_PROC.PRONAMESPACE.eq(oid(PG_NAMESPACE))) + .and(PG_PROC.PRONAME.concat("_").concat(oid(PG_PROC)).eq(r1.SPECIFIC_NAME)) .where(r1.ROUTINE_SCHEMA.in(getInputSchemata())) + .andNot(PG_PROC.PRORETSET) .orderBy( r1.ROUTINE_SCHEMA.asc(), r1.ROUTINE_NAME.asc()) diff --git a/jOOQ-meta/src/main/java/org/jooq/util/postgres/PostgresRoutineDefinition.java b/jOOQ-meta/src/main/java/org/jooq/util/postgres/PostgresRoutineDefinition.java index 538c0ca13e..643a53e7c2 100644 --- a/jOOQ-meta/src/main/java/org/jooq/util/postgres/PostgresRoutineDefinition.java +++ b/jOOQ-meta/src/main/java/org/jooq/util/postgres/PostgresRoutineDefinition.java @@ -91,6 +91,13 @@ public class PostgresRoutineDefinition extends AbstractRoutineDefinition { specificName = record.getValue(ROUTINES.SPECIFIC_NAME); } + // [#3375] This internal constructor is used for table-valued functions. It should not be used otherwise + PostgresRoutineDefinition(Database database, String schema, String name, String specificName) { + super(database.getSchema(schema), null, name, null, null); + + this.specificName = specificName; + } + @Override protected void init0() throws SQLException { for (Record record : create().select( diff --git a/jOOQ-meta/src/main/java/org/jooq/util/postgres/PostgresTableValuedFunction.java b/jOOQ-meta/src/main/java/org/jooq/util/postgres/PostgresTableValuedFunction.java new file mode 100644 index 0000000000..fbd07a3c4c --- /dev/null +++ b/jOOQ-meta/src/main/java/org/jooq/util/postgres/PostgresTableValuedFunction.java @@ -0,0 +1,143 @@ +/** + * Copyright (c) 2009-2014, Data Geekery GmbH (http://www.datageekery.com) + * All rights reserved. + * + * This work is dual-licensed + * - under the Apache Software License 2.0 (the "ASL") + * - under the jOOQ License and Maintenance Agreement (the "jOOQ License") + * ============================================================================= + * You may choose which license applies to you: + * + * - If you're using this work with Open Source databases, you may choose + * either ASL or jOOQ License. + * - If you're using this work with at least one commercial database, you must + * choose jOOQ License + * + * For more information, please visit http://www.jooq.org/licenses + * + * Apache Software License 2.0: + * ----------------------------------------------------------------------------- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * jOOQ License and Maintenance Agreement: + * ----------------------------------------------------------------------------- + * Data Geekery grants the Customer the non-exclusive, timely limited and + * non-transferable license to install and use the Software under the terms of + * the jOOQ License and Maintenance Agreement. + * + * This library is distributed with a LIMITED WARRANTY. See the jOOQ License + * and Maintenance Agreement for more details: http://www.jooq.org/licensing + */ + +package org.jooq.util.postgres; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.jooq.Record; +import org.jooq.util.AbstractTableDefinition; +import org.jooq.util.ColumnDefinition; +import org.jooq.util.DataTypeDefinition; +import org.jooq.util.DefaultColumnDefinition; +import org.jooq.util.DefaultDataTypeDefinition; +import org.jooq.util.ParameterDefinition; +import org.jooq.util.SchemaDefinition; + +/** + * @author Lukas Eder + */ +public class PostgresTableValuedFunction extends AbstractTableDefinition { + + private final PostgresRoutineDefinition routine; + private final String specificName; + + public PostgresTableValuedFunction(SchemaDefinition schema, String name, String specificName, String comment) { + super(schema, name, comment); + + this.routine = new PostgresRoutineDefinition(schema.getDatabase(), schema.getInputName(), name, specificName); + this.specificName = specificName; + } + + @Override + public List getElements0() throws SQLException { + List result = new ArrayList(); + Long oid = null; + + try { + oid = Long.valueOf(specificName.substring(specificName.lastIndexOf("_") + 1)); + } + catch (Exception ignore) {} + + if (oid != null) { + for (Record record : create().fetch( + + // [#3375] This query mimicks what SQL Server knows as INFORMATION_SCHEMA.COLUMNS for table-valued + // functions, which are really tables with result COLUMNS. + + " SELECT columns.proargname AS column_name," + + " ROW_NUMBER() OVER(PARTITION BY p.oid ORDER BY o.ordinal) AS ordinal_position," + + " format_type(t.oid, t.typtypmod) AS data_type," + + " information_schema._pg_char_max_length(t.oid, t.typtypmod) AS character_maximum_length," + + " information_schema._pg_numeric_precision(t.oid, t.typtypmod) AS numeric_precision," + + " information_schema._pg_numeric_scale(t.oid, t.typtypmod) AS numeric_scale," + + " not(t.typnotnull) AS is_nullable" + + " FROM pg_proc p," + + " LATERAL generate_series(1, array_length(p.proargmodes, 1)) o(ordinal)," + + " LATERAL (SELECT p.proargnames[o.ordinal], p.proargmodes[o.ordinal], p.proallargtypes[o.ordinal]) columns(proargname, proargmode, proargtype)," + + " LATERAL (" + + " SELECT pg_type.oid oid, pg_type.* FROM pg_type WHERE pg_type.oid = columns.proargtype" + + " ) t" + + " WHERE p.proretset" + + " AND columns.proargmode = 't'" + + " AND p.oid = ?", oid) + ) { + + DataTypeDefinition type = new DefaultDataTypeDefinition( + getDatabase(), + getSchema(), + record.getValue("data_type", String.class), + record.getValue("character_maximum_length", Integer.class), + record.getValue("numeric_precision", Integer.class), + record.getValue("numeric_scale", Integer.class), + record.getValue("is_nullable", boolean.class), + false, + null + ); + + ColumnDefinition column = new DefaultColumnDefinition( + getDatabase().getTable(getSchema(), getName()), + record.getValue("column_name", String.class), + record.getValue("ordinal_position", int.class), + type, + false, + null + ); + + result.add(column); + } + } + + return result; + } + + @Override + protected List getParameters0() { + return routine.getInParameters(); + } + + @Override + public boolean isTableValuedFunction() { + return true; + } +} diff --git a/jOOQ-test/src/main/resources/org/jooq/test/postgres/create.sql b/jOOQ-test/src/main/resources/org/jooq/test/postgres/create.sql index d5b1ed157d..9f4bf56d87 100644 --- a/jOOQ-test/src/main/resources/org/jooq/test/postgres/create.sql +++ b/jOOQ-test/src/main/resources/org/jooq/test/postgres/create.sql @@ -5,6 +5,11 @@ DROP VIEW IF EXISTS v_library/ DROP VIEW IF EXISTS v_author/ DROP VIEW IF EXISTS v_book/ +DROP FUNCTION f_tables1()/ +DROP FUNCTION f_tables2()/ +DROP FUNCTION f_tables3()/ +DROP FUNCTION f_tables4(in_id INTEGER)/ +DROP FUNCTION f_tables5 (v1 INTEGER, v2 INTEGER, v3 INTEGER)/ DROP FUNCTION f_arrays(in_array IN integer[])/ DROP FUNCTION f_arrays(in_array IN bigint[])/ DROP FUNCTION f_arrays(in_array IN text[])/ @@ -125,7 +130,7 @@ CREATE TABLE t_3111 ( inverse int, bool1 boolean, bool2 boolean, - + CONSTRAINT pk_t_3111 PRIMARY KEY (id) ) / @@ -458,7 +463,7 @@ CREATE TABLE t_exotic_types ( ID INT NOT NULL, UU UUID, JS JSON, - + CONSTRAINT pk_t_exotic_types PRIMARY KEY(ID) ) / @@ -514,7 +519,7 @@ CREATE TABLE x_test_case_85 ( CREATE TABLE x_test_case_2025 ( ref_id INTEGER NOT NULL, ref_name VARCHAR(10) NOT NULL, - + CONSTRAINT fk_x_test_case_2025_1 FOREIGN KEY(ref_id) REFERENCES x_test_case_85(ID), CONSTRAINT fk_x_test_case_2025_2 FOREIGN KEY(ref_id) REFERENCES x_test_case_71(ID), CONSTRAINT fk_x_test_case_2025_3 FOREIGN KEY(ref_id, ref_name) REFERENCES X_UNUSED(id, name) @@ -694,6 +699,74 @@ END; $$ LANGUAGE plpgsql; / +CREATE FUNCTION f_tables1 () +RETURNS TABLE ( + column_value INTEGER +) +AS $$ +BEGIN + RETURN QUERY + SELECT 1; +END; +$$ LANGUAGE plpgsql; +/ + +CREATE FUNCTION f_tables2 () +RETURNS TABLE ( + column_value BIGINT +) +AS $$ +BEGIN + RETURN QUERY + SELECT CAST(1 AS BIGINT); +END +$$ LANGUAGE plpgsql; +/ + +CREATE FUNCTION f_tables3 () +RETURNS TABLE ( + column_value VARCHAR(5) +) +AS $$ +BEGIN + RETURN QUERY + SELECT CAST('1' AS VARCHAR(5)); +END +$$ LANGUAGE plpgsql; +/ + +CREATE FUNCTION f_tables4 (in_id INTEGER) +RETURNS TABLE ( + id INTEGER, + title VARCHAR(400) +) +AS $$ +BEGIN + RETURN QUERY + SELECT b.id, b.title + FROM t_book b + WHERE in_id IS NULL OR b.id = in_id + ORDER BY b.id; +END +$$ LANGUAGE plpgsql; +/ + +CREATE FUNCTION f_tables5 (v1 INTEGER, v2 INTEGER, v3 INTEGER) +RETURNS TABLE ( + v INTEGER, + s INTEGER +) +AS $$ +BEGIN + RETURN QUERY + SELECT * + FROM (VALUES(v1, v1), + (v2, v1 + v2), + (v3, v1 + v2 + v3)) t(a, b); +END +$$ LANGUAGE plpgsql; +/ + CREATE FUNCTION f_author_exists (author_name VARCHAR) RETURNS INT AS $$ diff --git a/jOOQ-test/src/test/java/org/jooq/test/PostgresTest.java b/jOOQ-test/src/test/java/org/jooq/test/PostgresTest.java index 50f869f707..2d62d9b2da 100644 --- a/jOOQ-test/src/test/java/org/jooq/test/PostgresTest.java +++ b/jOOQ-test/src/test/java/org/jooq/test/PostgresTest.java @@ -45,11 +45,15 @@ import static java.util.Arrays.asList; import static org.jooq.conf.StatementType.STATIC_STATEMENT; import static org.jooq.impl.DSL.field; import static org.jooq.impl.DSL.inline; +import static org.jooq.impl.DSL.lateral; import static org.jooq.impl.DSL.name; import static org.jooq.impl.DSL.select; import static org.jooq.impl.DSL.selectOne; import static org.jooq.impl.DSL.val; -import static org.jooq.test.postgres.generatedclasses.Routines.fSearchBook; +import static org.jooq.test.postgres.generatedclasses.Tables.F_TABLES1; +import static org.jooq.test.postgres.generatedclasses.Tables.F_TABLES2; +import static org.jooq.test.postgres.generatedclasses.Tables.F_TABLES3; +import static org.jooq.test.postgres.generatedclasses.Tables.F_TABLES4; import static org.jooq.test.postgres.generatedclasses.Tables.T_3111; import static org.jooq.test.postgres.generatedclasses.Tables.T_639_NUMBERS_TABLE; import static org.jooq.test.postgres.generatedclasses.Tables.T_725_LOB_TEST; @@ -96,6 +100,7 @@ import org.jooq.Name; import org.jooq.Param; import org.jooq.Record; import org.jooq.Record1; +import org.jooq.Record2; import org.jooq.Record3; import org.jooq.Record5; import org.jooq.Result; @@ -120,6 +125,9 @@ import org.jooq.test.postgres.generatedclasses.Sequences; import org.jooq.test.postgres.generatedclasses.enums.UCountry; import org.jooq.test.postgres.generatedclasses.enums.U_959; import org.jooq.test.postgres.generatedclasses.tables.TArrays; +import org.jooq.test.postgres.generatedclasses.tables.records.FTables2Record; +import org.jooq.test.postgres.generatedclasses.tables.records.FTables3Record; +import org.jooq.test.postgres.generatedclasses.tables.records.FTables4Record; import org.jooq.test.postgres.generatedclasses.tables.records.TArraysRecord; import org.jooq.test.postgres.generatedclasses.tables.records.TAuthorRecord; import org.jooq.test.postgres.generatedclasses.tables.records.TBookRecord; @@ -992,14 +1000,51 @@ public class PostgresTest extends jOOQAbstractTest< @Test public void testPostgresTableFunction() throws Exception { + // TODO [#1139] [#3375] [#3376] Further elaborate this test +// create().select().from(fSearchBook("Animal", 1L, 0L).toString()).fetch(); +// System.out.println(create().select(fSearchBook("Animal", 1L, 0L)).fetch()); - // TODO [#1139] Further elaborate this test - create().select().from(fSearchBook("Animal", 1L, 0L).toString()).fetch(); - System.out.println(create().select(fSearchBook("Animal", 1L, 0L)).fetch()); + // Simple call with SELECT clause + Result> result1 = + create().select(F_TABLES1.COLUMN_VALUE) + .from(F_TABLES1.call()) + .fetch(); - // This doesn't work, as jOOQ doesn't know how to correctly register - // OUT parameters for the returned cursor - // Object result = Routines.fSearchBook(create(), "Animal", 1L, 0L); + assertEquals(1, result1.size()); + assertEquals(1, result1.get(0).size()); + assertEquals(1, (int) result1.getValue(0, F_TABLES1.COLUMN_VALUE)); + + // Typesafe call to get a TableRecord + FTables2Record result2 = + create().selectFrom(F_TABLES2.call()) + .fetchOne(); + + assertEquals(1L, (long) result2.getColumnValue()); + + FTables3Record result3 = + create().selectFrom(F_TABLES3.call()) + .fetchOne(); + + assertEquals("1", result3.getColumnValue()); + + // In parameters + Result result4a = + create().selectFrom(F_TABLES4.call(val(null, Integer.class))) + .fetch(); + + assertEquals(BOOK_IDS, result4a.getValues(F_TABLES4.ID)); + assertEquals(BOOK_TITLES, result4a.getValues(F_TABLES4.TITLE)); + + // Lateral JOIN + Result> result4b = + create().select(F_TABLES4.ID, F_TABLES4.TITLE) + .from(T_BOOK, lateral(F_TABLES4.call(T_BOOK.ID))) + .where(F_TABLES4.TITLE.like("%a%")) + .orderBy(F_TABLES4.ID) + .fetch(); + + assertEquals(BOOK_IDS.subList(1, 4), result4b.getValues(F_TABLES4.ID)); + assertEquals(BOOK_TITLES.subList(1, 4), result4b.getValues(F_TABLES4.TITLE)); } @Test diff --git a/jOOQ/src/main/java/org/jooq/impl/TableImpl.java b/jOOQ/src/main/java/org/jooq/impl/TableImpl.java index 398bf3ba04..1d55ea57ac 100644 --- a/jOOQ/src/main/java/org/jooq/impl/TableImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/TableImpl.java @@ -44,6 +44,7 @@ package org.jooq.impl; import static org.jooq.Clause.TABLE; import static org.jooq.Clause.TABLE_ALIAS; import static org.jooq.Clause.TABLE_REFERENCE; +import static org.jooq.SQLDialect.POSTGRES; import java.util.Arrays; @@ -69,9 +70,10 @@ public class TableImpl extends AbstractTable { private static final Clause[] CLAUSES_TABLE_ALIAS = { TABLE, TABLE_ALIAS }; private final Fields fields; - protected final Field[] parameters; private final Alias> alias; + protected final Field[] parameters; + public TableImpl(String name) { this(name, null, null, null, null); } @@ -130,7 +132,11 @@ public class TableImpl extends AbstractTable { alias.accept(ctx); } else { - if (ctx.qualify()) { + if (ctx.qualify() + + // [#3375] PostgreSQL table-valued function references must not include the schema, e.g. in the + // SELECT list. Only in the FROM clause this is permitted. + && ((parameters != null && ctx.declareTables()) || ctx.configuration().dialect().family() != POSTGRES)) { Schema mappedSchema = Utils.getMappedSchema(ctx.configuration(), getSchema()); if (mappedSchema != null) {