pgEdge Platform Support for Large Object Logical Replication
Replication of large objects isn't currently supported by the community version of PostgreSQL logical replication. If you try to replicate a large object with logical replication, PostgreSQL will return: Large objects aren’t supported by logical replication
. It's a meaningful error (always nice), but not helpful if you have large objects that you need to replicate.
pgEdge has developed an extension named LargeObjectLOgicalReplication (LOLOR) that provides support for replicating large objects. The primary goal of LOLOR is to provide seamless replication of large objects with pgEdge Spock multi-master distributed replication.
You can access and manipulate large objects in a PostgreSQL database with the following client interface functions:
lo_create
lo_import
lo_import_with_oid
lo_open
lo_export
lo_read
lo_write
lo_seek
lo_seek64
The pgEdge LOLOR extension supports the same large objects functions put in place by PostgreSQL, so all of your existing applications that use the previously mentioned functions will continue to work seamlessly.
The easiest way to install the LOLOR extension is with pgEdge Platform. After installing pgEdge Platform, you can use pgEdge Platform to install LOLOR, create the extension, and add it to the shared_preload_libraries
parameter by navigating into the pgedge
installation directory and running the command:
./pgedge install lolor
In this blog, we are going to create a two node pgEdge cluster on the localhost to demonstrate how pgEdge Platform replicates large objects. We'll also share a native PSQL example of using the extension for replicating large objects, and a JDBC example that shows how we can use the extension from a Java program using a JDBC driver.
In any directory owned by your non-root user, use the following command to install pgEdge on all nodes of the cluster; you'll need to invoke this command on each replication node host:
python3 -c "$(curl -fsSL https://pgedge-download.s3.amazonaws.com/REPO/install.py)"
Node 1 setup
Navigate into the pgedge
directory on node 1 and perform the following steps :
Run the following command to set up the pgEdge platform; this command installs PostgreSQL version 16 and the pgEdge Spock and Snowflake extensions.
./pgedge setup -U demo -P pgedge110 -d testdb -p 5432
-U is the name of the database superuser
-P is the password for the database user
-d is the name of the database
-p is the port number (5432 is default PostgreSQL port)
Then, run the following command to create a Spock node (we are creating a node named n1
). Note that user named in the command below (in our command pgedge
) needs to be an OS user:
pgedge spock node-create n1 'host=localhost user=pgedge dbname=testdb' testdb
The next command creates the subscription between n1
and n2
. You should run this command after completing the initial pgEdge setup on n2
.
pgedge spock sub-create sub_n1n2 ‘host=localhost port=5444 user=pgedge dbname=testdb’ testdb
Then, use the following command to install the LOLOR extension :
pgedge install lolor
Then, source your PostgreSQL installation, connect with PSQL, and run the CREATE EXTENSION
statement to create the LOLOR extension:
source pgedge/pg16/pg16.env
PSQL -U demo -d testdb -p 5432
CREATE EXTENSION lolor;
You'll also need to set the lolor.node
configuration parameter before using the extension. Set the value to the number that corresponds to the node on which you're setting the parameter; the value can be from 1 to 2^28.
lolor.node=1
Please restart the server after adding the above configuration parameter to the postgresql.conf
file. The postgresql.conf file is located in the data directory under your PostgreSQL installation.
Before using LOLOR functionality, you also need to add the large object catalog tables to the default
replication set. You can use the following commands:
./pgedge spock repset-add-table default 'lolor.pg_largeobject' testdb
./pgedge spock repset-add-table default 'lolor.pg_largeobject_metadata' testdb
The following commands are executed to enable automatic DDL replication :
./pgedge db guc-set spock.include_ddl_repset on
./pgedge db guc-set spock.allow_ddl_from_functions on
./pgedge db guc-set spock.include_ddl_repset on
Node 2 setup
Navigate into the pgedge
directory on node 2 and perform the following steps to configure the LOLOR extension:
Run the following command to install pgEdge Platform, this will install PG-16, and the pgEdge Spock and Snowflake extensions.
./pgedge setup -U demo -P pgedge110 -d testdb -p 5444
-U is the database superuser
-P is the password for the database user
-d is the name of the database
-p is the port number (5444)
Use the following command to create a Spock node. Please note that the user provided in the following command needs to be a OS user :
./pgedge spock node-create n2 'host=localhost user=pgedge port=5444 dbname=testdb' testdb
Then, use the following command to create the subscription between n2
and n1
:
./pgedge spock sub-create sub_n2n1 ‘host=localhost port=5432 user=pgedge dbname=testdb’ testdb
Now we are ready to install the LOLOR extension with the command:
./pgedge install lolor
Then, log in PSQL and invoke the CREATE EXTENSION
statement:
./psql -U demo testdb -p 5444
CREATE EXTENSION lolor;
You must set lolor.node
to a number that represents the node in the replication cluster before using LOLOR. Acceptable values range from 1 to 2^28.
lolor.node=2
Please restart the server after adding the above configuration parameter to postgresql.conf
.
After setting the lolor.node
parameter, use the following commands to add the large object catalog tables to the default
replication set:
./pgedge spock repset-add-table default 'lolor.pg_largeobject' testdb
./pgedge spock repset-add-table default 'lolor.pg_largeobject_metadata' testdb
Then, execute the following commands to enable automatic DDL replication :
./pgedge db guc-set spock.enable_ddl_replication on
./pgedge db guc-set spock.allow_ddl_from_functions on
./pgedge db guc-set spock.include_ddl_repset on
Example: Using the PSQL Command Line to Exercise LOLOR
In the sections that follow, we are going to do a short test that demonstrates large object replication using the PSQL client. PSQL is a secure, native PostgreSQL client that uses the libpq driver to negotiate connections.
First, we are going to perform the following SQL commands on node 1:
create table test_lolor(id int primary key, lo_object oid);
INSERT INTO test_lolor VALUES
(008, lo_from_bytea(0, '\xaced0005774d0a060805100418005243080010001a3918002000320608011000180042121a080a044d41494e100022060a044d41494e52090a0744454641554c545a0c0a0a0805320608001000180122026800'));
testdb=# select id,lo_get(lo_object) from test_lolor;
id | lo_get
----+---------------------------------------------------------------------------------------------------------------------------------------------------------
-----------------
8 | \xaced0005774d0a060805100418005243080010001a3918002000320608011000180042121a080a044d41494e100022060a044d41494e52090a0744454641554c545a0c0a0a080532060800
1000180122026800
(1 row)
We have auto_ddl enabled so the table is also getting replicated to other nodes. We can query node 2 with the following SELECT
statement to confirm that the large object was replicated:
testdb=# select id,lo_get(lo_object) from test_lolor;
id | lo_get
----+---------------------------------------------------------------------------------------------------------------------------------------------------------
-----------------
8 | \xaced0005774d0a060805100418005243080010001a3918002000320608011000180042121a080a044d41494e100022060a044d41494e52090a0744454641554c545a0c0a0a080532060800
1000180122026800
(1 row)
Example: Using a JDBC Connection to Query a Large Object
The following program code connects with a pgEdge node and loads /etc/os-release
file in the database as a large object and perform retrieval operations.
To simplify connection management, you can specify connection information in the app.properties file, and then reference the file in your JDBC connection.
app.properties
# JDBC
Jdbc.drivers=org.postgresql.Driver
url=jdbc:postgresql://localhost:5432/lolordb
username=asif
password=password
example.java
package lolor;
import java.sql.*;
import java.io.*;
import java.util.Properties;
import java.nio.charset.StandardCharsets;
import org.postgresql.PGConnection;
import org.postgresql.largeobject.LargeObject;
import org.postgresql.largeobject.LargeObjectManager;
public class Example {
private static Connection pgconn = null;
private static Properties dbProps;
private final static String dbPropsFile = "app.properties";
/*
* load property file
*/
public static void loadDBPropertiesFile() throws Exception {
dbProps = new Properties();
InputStream in = new FileInputStream(dbPropsFile);
dbProps.load(in);
in.close();
}
/*
* Connect with PG
*/
public static void connectPG()
throws Exception {
try {
// Set the search_path to pick lolor schema first
dbProps.setProperty("options", "-c search_path=lolor,\"$user\",public,pg_catalog");
pgconn = DriverManager.getConnection(dbProps.getProperty("url"), dbProps);
pgconn.setAutoCommit(false);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/*
* Close the connection
*/
public static void disconnectPG()
throws Exception {
try {
pgconn.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/*
* Run query and return results
* Perform commit if asked
*/
public static String executeSQL(String sql, boolean doCommit)
throws Exception {
String result = "";
try {
StringBuilder sbResult = new StringBuilder();
PreparedStatement ps = pgconn.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
ResultSetMetaData rsmd = rs.getMetaData();
int columnsNumber = rsmd.getColumnCount();
for (int i = 1; i <= columnsNumber; i++) {
if (i > 1)
sbResult.append(",");
sbResult.append(rsmd.getColumnName(i));
}
sbResult.append("\n");
if (true) {
while (rs.next()) {
for (int i = 1; i <= columnsNumber; i++) {
if (i > 1)
sbResult.append(",");
String columnValue = rs.getString(i);
sbResult.append(columnValue);
}
sbResult.append("\n");
}
}
if (doCommit) {
pgconn.commit();
}
result = sbResult.toString();
return result;
} catch (SQLException e) {
// 02000 = no_data
if (e.getSQLState().compareTo("02000") == 0) {
return result;
} else {
pgconn.rollback();
throw new RuntimeException(e);
}
}
}
/*
* Initialize database
*/
public static void initDB()
throws Exception {
executeSQL("DROP TABLE pglotest_blobs;", true);
String createTableSql = "CREATE TABLE pglotest_blobs (\n" +
" fname text PRIMARY KEY,\n" +
" blob oid\n" +
");";
executeSQL(createTableSql, true);
executeSQL("CREATE EXTENSION IF NOT EXISTS lolor;", true);
}
/*
* Perform insert operation
* It internally calls lo_create, lo_open, lo_write, lo_close
* */
static byte[] do_insert(String fname)
throws Exception {
File file;
FileInputStream fis;
LargeObjectManager lom;
long oid;
LargeObject lo;
byte[] buf = new byte[10];
int n;
ByteArrayOutputStream byteArrayOutStr = new ByteArrayOutputStream();
// Open the input file as InputStream
file = new File(fname);
fis = new FileInputStream(file);
// Create the LO
lom = ((PGConnection) pgconn).getLargeObjectAPI();
oid = lom.createLO();
lo = lom.open(oid, LargeObjectManager.WRITE);
while ((n = fis.read(buf, 0, buf.length)) > 0) {
lo.write(buf, 0, n);
byteArrayOutStr.write(buf, 0, n);
}
lo.close();
// Create the entry in the pglotest_blobs table
PreparedStatement ps = pgconn.prepareStatement("INSERT INTO pglotest_blobs VALUES (?, ?)");
ps.setString(1, fname);
ps.setLong(2, oid);
ps.execute();
ps.close();
// Close the input file and commit the transaction
fis.close();
pgconn.commit();
return byteArrayOutStr.toByteArray();
}
/*
* Perform read operation
* It internally calls lo_open, loread, lo_close
*/
static byte[] do_select(String fname)
throws Exception {
LargeObjectManager lom;
long oid;
LargeObject lo;
byte[] buf = new byte[10];
int n;
ByteArrayOutputStream byteArrayOutStr = new ByteArrayOutputStream();
// Get the OID of the LO with that filename
PreparedStatement ps = pgconn.prepareStatement("SELECT blob FROM pglotest_blobs WHERE fname = ?");
ps.setString(1, fname);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
// Open the LO and read its content
oid = rs.getLong(1);
lom = ((PGConnection) pgconn).getLargeObjectAPI();
lo = lom.open(oid, LargeObjectManager.READ);
while ((n = lo.read(buf, 0, buf.length)) > 0) {
byteArrayOutStr.write(buf, 0, n);
}
lo.close();
} else {
throw new Exception("Entry for " + fname + " not found");
}
// Rollback the transaction
pgconn.rollback();
return byteArrayOutStr.toByteArray();
}
public static void main(String[] args) throws Exception {
// Pick a sample file
String textFile1 = "/etc/os-release";
// Initialization
loadDBPropertiesFile();
connectPG();
initDB();
// Perform LO operations
byte[] bufInput = do_insert(textFile1);
byte[] bufRetrieved = do_select(textFile1);
// Verify the results
String input = new String(bufInput, StandardCharsets.UTF_8);
String retrieved = new String(bufRetrieved, StandardCharsets.UTF_8);
System.out.println("-----------------------");
System.out.println("Text Input: ");
System.out.println("-----------------------\n" + input);
System.out.println("-----------------------");
System.out.println("Text Retrieved: ");
System.out.println("-----------------------\n" + retrieved);
}