Add support for MySQL and Postgres testing.

* Introduce Testcontainers as the mechanism to test against real data stores.
* Alter CI to handle running Testcontainers in reduced privilege mode.
* Run the data store specific test cases ONLY for the baseline Java 8 test execution.

See #2256.
This commit is contained in:
Greg L. Turnquist
2021-07-22 16:57:58 -05:00
parent 183e3e5b62
commit a4ef56d83c
9 changed files with 698 additions and 9 deletions

7
Jenkinsfile vendored
View File

@@ -24,13 +24,16 @@ pipeline {
}
options { timeout(time: 30, unit: 'MINUTES') }
environment {
DOCKER_HUB = credentials('hub.docker.com-springbuildmaster')
ARTIFACTORY = credentials('02bd1690-b54f-4c9f-819d-a77cb7a9822c')
}
steps {
script {
docker.withRegistry('', 'hub.docker.com-springbuildmaster') {
docker.image('adoptopenjdk/openjdk8:latest').inside('-v $HOME:/tmp/jenkins-home') {
sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -s settings.xml clean dependency:list test -Dsort -Dbundlor.enabled=false -U -B'
docker.image('adoptopenjdk/openjdk8:latest').inside('-u root -v /var/run/docker.sock:/var/run/docker.sock -v /usr/bin/docker:/usr/bin/docker -v $HOME:/tmp/jenkins-home') {
sh "docker login --username ${DOCKER_HUB_USR} --password ${DOCKER_HUB_PSW}"
sh 'PROFILE=all-dbs ci/test.sh'
sh "ci/clean.sh"
}
}
}

6
ci/clean.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash -x
set -euo pipefail
MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" \
./mvnw -s settings.xml clean -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-jpa

10
ci/test.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash -x
set -euo pipefail
mkdir -p /tmp/jenkins-home/.m2/spring-data-jpa
chown -R 1001:1001 .
MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" \
./mvnw -s settings.xml \
-P${PROFILE} clean dependency:list test -Dsort -U -B -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-jpa

99
pom.xml
View File

@@ -23,9 +23,12 @@
<eclipselink>2.7.9</eclipselink>
<hibernate>5.5.3.Final</hibernate>
<vavr>0.10.3</vavr>
<hibernate.groupId>org.hibernate</hibernate.groupId>
<mysql-connector-java>8.0.23</mysql-connector-java>
<postgresql>42.2.19</postgresql>
<springdata.commons>2.6.0-SNAPSHOT</springdata.commons>
<vavr>0.10.3</vavr>
<hibernate.groupId>org.hibernate</hibernate.groupId>
<java-module-name>spring.data.jpa</java-module-name>
@@ -34,6 +37,43 @@
</properties>
<profiles>
<profile>
<id>all-dbs</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<executions>
<execution>
<id>mysql-test</id>
<phase>test</phase>
<goals>
<goal>test</goal>
</goals>
<configuration>
<includes>
<include>**/MySql*IntegrationTests.java</include>
</includes>
</configuration>
</execution>
<execution>
<id>postgres-test</id>
<phase>test</phase>
<goals>
<goal>test</goal>
</goals>
<configuration>
<includes>
<include>**/Postgres*IntegrationTests.java</include>
</includes>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>hibernate-next</id>
<properties>
@@ -116,6 +156,18 @@
</profile>
</profiles>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>${testcontainers}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
@@ -187,6 +239,40 @@
<scope>test</scope>
</dependency>
<!-- MySQL testing support -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector-java}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Postgres testing support -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.threeten</groupId>
<artifactId>threetenbp</artifactId>
@@ -325,6 +411,7 @@
</dependencies>
<executions>
<execution>
<!-- override the default-test execution and exclude everything -->
<id>default-test</id>
<configuration>
<excludes>
@@ -333,7 +420,7 @@
</configuration>
</execution>
<execution>
<id>unit-tests</id>
<id>unit-test</id>
<goals>
<goal>test</goal>
</goals>
@@ -345,7 +432,7 @@
</configuration>
</execution>
<execution>
<id>integration-tests</id>
<id>integration-test</id>
<goals>
<goal>test</goal>
</goals>
@@ -359,6 +446,8 @@
<exclude>**/*UnitTests.java</exclude>
<exclude>**/OpenJpa*</exclude>
<exclude>**/EclipseLink*</exclude>
<exclude>**/MySql*</exclude>
<exclude>**/Postgres*</exclude>
</excludes>
<argLine>
-javaagent:${settings.localRepository}/org/springframework/spring-instrument/${spring}/spring-instrument-${spring}.jar
@@ -367,7 +456,7 @@
</configuration>
</execution>
<execution>
<id>eclipselink-tests</id>
<id>eclipselink-test</id>
<goals>
<goal>test</goal>
</goals>

View File

@@ -0,0 +1,252 @@
/*
* Copyright 2015-2021 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.data.jpa.repository.procedures;
import static org.assertj.core.api.Assertions.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Properties;
import javax.persistence.Entity;
import javax.persistence.EntityManagerFactory;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.NamedStoredProcedureQuery;
import javax.sql.DataSource;
import javax.transaction.Transactional;
import org.hibernate.dialect.MySQL8Dialect;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.jpa.repository.query.Procedure;
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.testcontainers.containers.MySQLContainer;
import com.mysql.cj.jdbc.MysqlDataSource;
/**
* Testcase to verify {@link org.springframework.jdbc.object.StoredProcedure}s work with MySQL.
*
* @author Gabriel Basilio
* @author Greg Turnquist
*/
@Transactional
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = MySqlStoredProcedureIntegrationTests.Config.class)
public class MySqlStoredProcedureIntegrationTests {
@Autowired EmployeeRepositoryWithNoCursor repository;
@Test // #2256
void testGenericSingleObjectFromResultSet() {
Object[] employee = repository.genericSingleObjectFromResultSet();
assertThat(employee).containsExactly( //
new Object[] { 3, "Fanny" }, //
new Object[] { 4, "Gabriel" });
}
@Test // #2256
void testGenericObjectsFromResultSet() {
List<Object[]> employees = repository.genericObjectsFromResultSet();
assertThat(employees).containsExactly( //
new Object[] { 3, "Fanny" }, //
new Object[] { 4, "Gabriel" });
}
@Test // #2256
void testEntityListFromResultSet() {
List<Employee> employees = repository.entityListFromResultSet();
assertThat(employees).containsExactly( //
new Employee(3, "Fanny"), //
new Employee(4, "Gabriel"));
}
@Test // #2256
void testNamedOutputParameter() {
List<Employee> employees = repository.namedOutputParameter();
assertThat(employees).containsExactly( //
new Employee(3, "Fanny"), //
new Employee(4, "Gabriel"));
}
@Test // #2256
void testSingleEntityFromResultSet() {
Employee employee = repository.singleEntityFromResultSet();
assertThat(employee).isEqualTo(new Employee(3, "Fanny"));
}
@Test // #2256
void testEntityListFromSingleRowResultSet() {
List<Employee> employees = repository.entityListFromSingleRowResultSet();
assertThat(employees).containsExactly(new Employee(3, "Fanny"));
}
@Test // #2256
void testNoResultSet() {
int count = repository.noResultSet();
assertThat(count).isEqualTo(2);
}
@Test // #2256
void testEntityListFromNamedProcedure() {
List<Employee> employees = repository.entityListFromNamedProcedure();
assertThat(employees).containsExactly( //
new Employee(3, "Fanny"), //
new Employee(4, "Gabriel"));
}
@Data
@Entity
@AllArgsConstructor
@NoArgsConstructor
@NamedStoredProcedureQuery(name = "get_employees_mysql", procedureName = "get_employees",
resultClasses = Employee.class)
public static class Employee {
@Id @GeneratedValue private Integer id;
private String name;
}
@Transactional
public interface EmployeeRepositoryWithNoCursor extends JpaRepository<Employee, Integer> {
@Procedure(value = "get_employees")
Object[] genericSingleObjectFromResultSet();
@Procedure(value = "get_employees")
List<Object[]> genericObjectsFromResultSet();
@Procedure(value = "get_employees")
List<Employee> entityListFromResultSet();
@Procedure(value = "get_employees", outputParameterName = "p_employees")
List<Employee> namedOutputParameter();
@Procedure(value = "get_single_employee")
Employee singleEntityFromResultSet();
@Procedure(value = "get_single_employee")
List<Employee> entityListFromSingleRowResultSet();
@Procedure(value = "get_employees_count")
Integer noResultSet();
@Procedure(name = "get_employees_mysql")
List<Employee> entityListFromNamedProcedure();
}
@EnableJpaRepositories(considerNestedRepositories = true, basePackageClasses = Config.class,
includeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = EmployeeRepositoryWithNoCursor.class))
@EnableTransactionManagement
static class Config {
private MySQLContainer<?> MYSQL_CONTAINER;
@Bean
public DataSource dataSource() {
if (MYSQL_CONTAINER == null) {
MYSQL_CONTAINER = new MySQLContainer<>("mysql:8.0.24") //
.withUsername("test") //
.withPassword("test") //
.withConfigurationOverride("");
MYSQL_CONTAINER.start();
}
MysqlDataSource dataSource = new MysqlDataSource();
dataSource.setUrl(MYSQL_CONTAINER.getJdbcUrl());
dataSource.setUser("root");
dataSource.setPassword(MYSQL_CONTAINER.getPassword());
dataSource.setDatabaseName(MYSQL_CONTAINER.getDatabaseName());
return dataSource;
}
@Bean
public AbstractEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setPersistenceUnitRootLocation("simple-persistence");
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
factoryBean.setPackagesToScan(this.getClass().getPackage().getName());
Properties properties = new Properties();
properties.setProperty("hibernate.hbm2ddl.auto", "create");
properties.setProperty("hibernate.dialect", MySQL8Dialect.class.getCanonicalName());
factoryBean.setJpaProperties(properties);
return factoryBean;
}
@Bean
PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
@Bean
DataSourceInitializer initializer(DataSource dataSource) {
DataSourceInitializer initializer = new DataSourceInitializer();
initializer.setDataSource(dataSource);
ClassPathResource script = new ClassPathResource("scripts/mysql-stored-procedures.sql");
ResourceDatabasePopulator populator = new ResourceDatabasePopulator(script);
populator.setSeparator(";;");
initializer.setDatabasePopulator(populator);
return initializer;
}
}
}

View File

@@ -0,0 +1,252 @@
/*
* Copyright 2015-2021 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.data.jpa.repository.procedures;
import static org.assertj.core.api.Assertions.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.List;
import java.util.Properties;
import javax.persistence.Entity;
import javax.persistence.EntityManagerFactory;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.NamedStoredProcedureQuery;
import javax.persistence.ParameterMode;
import javax.persistence.StoredProcedureParameter;
import javax.sql.DataSource;
import javax.transaction.Transactional;
import org.hibernate.dialect.PostgreSQL91Dialect;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.postgresql.ds.PGSimpleDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.jpa.repository.query.Procedure;
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.testcontainers.containers.PostgreSQLContainer;
/**
* Testcase to verify {@link org.springframework.jdbc.object.StoredProcedure}s work with Postgres.
*
* @author Gabriel Basilio
* @author Greg Turnquist
*/
@Transactional
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = PostgresStoredProcedureIntegrationTests.Config.class)
public class PostgresStoredProcedureIntegrationTests {
@Autowired EmployeeRepositoryWithRefCursor repository;
@Test // 2256
void testGenericSingleObjectFromResultSet() {
Object[] employee = repository.genericSingleObjectFromResultSet();
assertThat(employee).containsExactly( //
new Object[] { new BigDecimal("3"), "Fanny" }, //
new Object[] { new BigDecimal("4"), "Gabriel" });
}
@Test // 2256
void testGenericObjectsFromResultSet() {
List<Object[]> employees = repository.genericObjectsFromResultSet();
assertThat(employees).containsExactly( //
new Object[] { new BigDecimal("3"), "Fanny" }, //
new Object[] { new BigDecimal("4"), "Gabriel" });
}
@Test // 2256
void testEntityListFromResultSet() {
List<Employee> employees = repository.entityListFromResultSet();
assertThat(employees).containsExactly( //
new Employee(3, "Fanny"), //
new Employee(4, "Gabriel"));
}
@Test // 2256
void testNamedOutputParameter() {
List<Employee> employees = repository.namedOutputParameter();
assertThat(employees).containsExactly( //
new Employee(3, "Fanny"), //
new Employee(4, "Gabriel"));
}
@Test // 2256
void testSingleEntityFromResultSet() {
Employee employee = repository.singleEntityFromResultSet();
assertThat(employee).isEqualTo(new Employee(3, "Fanny"));
}
@Test // 2256
void testEntityListFromSingleRowResultSet() {
List<Employee> employees = repository.entityListFromSingleRowResultSet();
assertThat(employees).containsExactly(new Employee(3, "Fanny"));
}
@Test // 2256
void testNoResultSet() {
int count = repository.noResultSet();
assertThat(count).isEqualTo(2);
}
@Test // 2256
void testEntityListFromNamedProcedure() {
List<Employee> employees = repository.entityListFromNamedProcedure();
assertThat(employees).containsExactly( //
new Employee(3, "Fanny"), //
new Employee(4, "Gabriel"));
}
@Data
@Entity
@AllArgsConstructor
@NoArgsConstructor
@NamedStoredProcedureQuery(name = "get_employees_postgres", procedureName = "get_employees",
parameters = { @StoredProcedureParameter(mode = ParameterMode.REF_CURSOR, type = void.class) },
resultClasses = Employee.class)
public static class Employee {
@Id @GeneratedValue private Integer id;
private String name;
}
@Transactional
public interface EmployeeRepositoryWithRefCursor extends JpaRepository<Employee, Integer> {
@Procedure(value = "get_employees", refCursor = true)
Object[] genericSingleObjectFromResultSet();
@Procedure(value = "get_employees", refCursor = true)
List<Object[]> genericObjectsFromResultSet();
@Procedure(value = "get_employees", refCursor = true)
List<Employee> entityListFromResultSet();
@Procedure(value = "get_employees", outputParameterName = "p_employees", refCursor = true)
List<Employee> namedOutputParameter();
@Procedure(value = "get_single_employee", refCursor = true)
Employee singleEntityFromResultSet();
@Procedure(value = "get_single_employee", refCursor = true)
List<Employee> entityListFromSingleRowResultSet();
@Procedure(value = "get_employees_count")
Integer noResultSet();
@Procedure(name = "get_employees_postgres", refCursor = true)
List<Employee> entityListFromNamedProcedure();
}
@EnableJpaRepositories(considerNestedRepositories = true,
includeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = EmployeeRepositoryWithRefCursor.class))
@EnableTransactionManagement
static class Config {
private PostgreSQLContainer<?> POSTGRESQL_CONTAINER;
@Bean
public DataSource dataSource() {
if (POSTGRESQL_CONTAINER == null) {
POSTGRESQL_CONTAINER = new PostgreSQLContainer<>("postgres:9.6.12") //
.withUsername("postgres");
POSTGRESQL_CONTAINER.start();
}
PGSimpleDataSource dataSource = new PGSimpleDataSource();
dataSource.setUrl(POSTGRESQL_CONTAINER.getJdbcUrl());
dataSource.setUser(POSTGRESQL_CONTAINER.getUsername());
dataSource.setPassword(POSTGRESQL_CONTAINER.getPassword());
return dataSource;
}
@Bean
public AbstractEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setPersistenceUnitRootLocation("simple-persistence");
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
factoryBean.setPackagesToScan(this.getClass().getPackage().getName());
Properties properties = new Properties();
properties.setProperty("hibernate.hbm2ddl.auto", "create");
properties.setProperty("hibernate.dialect", PostgreSQL91Dialect.class.getCanonicalName());
factoryBean.setJpaProperties(properties);
return factoryBean;
}
@Bean
PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
@Bean
DataSourceInitializer initializer(DataSource dataSource) {
DataSourceInitializer initializer = new DataSourceInitializer();
initializer.setDataSource(dataSource);
ClassPathResource script = new ClassPathResource("scripts/postgres-stored-procedures.sql");
ResourceDatabasePopulator populator = new ResourceDatabasePopulator(script);
populator.setSeparator(";;");
initializer.setDatabasePopulator(populator);
return initializer;
}
}
}

View File

@@ -7,10 +7,15 @@
</encoder>
</appender>
<logger name="org.springframework.data" level="error" />
<logger name="org.springframework.data" level="error"/>
<!-- Uncomment these sections to debug -->
<!-- <logger name="org.springframework.data.jpa" level="trace" />-->
<!-- <logger name="org.springframework.jdbc" level="debug" />-->
<!-- <logger name="org.testcontainers" level="debug" />-->
<root level="error">
<appender-ref ref="console" />
<appender-ref ref="console"/>
</root>
</configuration>

View File

@@ -0,0 +1,27 @@
CREATE TABLE employee
(
ID int NOT NULL,
name varchar(50) NOT NULL,
primary key (ID)
);;
INSERT INTO employee (ID, NAME) VALUES (3, 'Fanny');;
INSERT INTO employee (ID, NAME) VALUES (4, 'Gabriel');;
DROP PROCEDURE IF EXISTS get_employees;;
CREATE PROCEDURE get_employees()
BEGIN
SELECT * FROM employee;
END;;
DROP PROCEDURE IF EXISTS get_employees_count;;
CREATE PROCEDURE get_employees_count(OUT employees INT)
BEGIN
SELECT COUNT(*) into employees FROM employee;
END;;
DROP PROCEDURE IF EXISTS get_single_employee;;
CREATE PROCEDURE get_single_employee()
BEGIN
SELECT * FROM employee WHERE employee.ID = 3;
END;;

View File

@@ -0,0 +1,45 @@
CREATE TABLE employee
(
ID numeric NOT NULL,
name text COLLATE pg_catalog."default" NOT NULL,
CONSTRAINT employee_pkey PRIMARY KEY (ID)
);;
INSERT INTO employee (ID, NAME) VALUES (3, 'Fanny');;
INSERT INTO employee (ID, NAME) VALUES (4, 'Gabriel');;
CREATE OR REPLACE FUNCTION get_employees()
RETURNS refcursor
LANGUAGE 'plpgsql'
AS
$BODY$
DECLARE
ref refcursor;
BEGIN
OPEN ref FOR SELECT * FROM employee;
RETURN ref;
END;
$BODY$;;
CREATE OR REPLACE FUNCTION get_employees_count()
RETURNS integer
LANGUAGE 'plpgsql'
AS
$BODY$
BEGIN
RETURN (SELECT COUNT(*) FROM employee);
END;
$BODY$;;
CREATE OR REPLACE FUNCTION get_single_employee()
RETURNS refcursor
LANGUAGE 'plpgsql'
AS
$BODY$
DECLARE
ref refcursor;
BEGIN
OPEN ref FOR SELECT * FROM employee WHERE employee.ID = 3;
RETURN ref;
END;
$BODY$;;