001/*
002 * Copyright (c) 2004-2025 Tada AB and other contributors, as listed below.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the The BSD 3-Clause License
006 * which accompanies this distribution, and is available at
007 * http://opensource.org/licenses/BSD-3-Clause
008 *
009 * Contributors:
010 *   Tada AB
011 *   Chapman Flack
012 */
013package org.postgresql.pljava.example.annotation;
014
015import java.sql.Connection;
016import java.sql.Date;
017import java.sql.DriverManager;
018import java.sql.PreparedStatement;
019import java.sql.ResultSet;
020import java.sql.SQLException;
021import java.sql.Savepoint;
022import java.sql.Statement;
023import java.sql.Time;
024import java.util.logging.Logger;
025
026import org.postgresql.pljava.SavepointListener;
027import org.postgresql.pljava.Session;
028import org.postgresql.pljava.SessionManager;
029import org.postgresql.pljava.TransactionListener;
030
031import org.postgresql.pljava.annotation.Function;
032import static org.postgresql.pljava.annotation.Function.Effects.*;
033import org.postgresql.pljava.annotation.SQLAction;
034
035/**
036 * Some methods used for testing the SPI JDBC driver.
037 *
038 * @author Thomas Hallgren
039 */
040@SQLAction(provides = "employees tables", install = {
041    "CREATE TABLE javatest.employees1" +
042    " (" +
043    " id     int PRIMARY KEY," +
044    " name   varchar(200)," +
045    " salary int" +
046    " )",
047
048    "CREATE TABLE javatest.employees2" +
049    " (" +
050    " id        int PRIMARY KEY," +
051    " name  varchar(200)," +
052    " salary    int," +
053    " transferDay date," +
054    " transferTime time" +
055    " )"
056    }, remove = {
057    "DROP TABLE javatest.employees2",
058    "DROP TABLE javatest.employees1"
059}
060)
061@SQLAction(requires = "issue228", install = "SELECT javatest.issue228()")
062public class SPIActions {
063    private static final String SP_CHECKSTATE = "sp.checkState";
064
065    private static final SavepointListener spListener = new SavepointListener() {
066        @Override
067        public void onAbort(Session session, Savepoint savepoint,
068                Savepoint parent) throws SQLException {
069            log("Abort of savepoint " + savepoint.getSavepointId());
070            nextState(session, 3, 0);
071        }
072
073        @Override
074        public void onCommit(Session session, Savepoint savepoint,
075                Savepoint parent) throws SQLException {
076            log("Commit of savepoint " + savepoint.getSavepointId());
077            nextState(session, 3, 4);
078        }
079
080        @Override
081        public void onStart(Session session, Savepoint savepoint,
082                Savepoint parent) throws SQLException {
083            log("Start of savepoint " + savepoint.getSavepointId());
084            nextState(session, 0, 1);
085        }
086    };
087
088    @Function(schema="javatest", effects=STABLE)
089    public static String getDateAsString() throws SQLException {
090        ResultSet rs = null;
091        Statement stmt = null;
092        Connection conn = DriverManager
093                .getConnection("jdbc:default:connection");
094        try {
095            stmt = conn.createStatement();
096            rs = stmt.executeQuery("SELECT CURRENT_DATE");
097            if (rs.next())
098                return rs.getDate(1).toString();
099            return "Date could not be retrieved";
100        } finally {
101            if (rs != null)
102                rs.close();
103            if (stmt != null)
104                stmt.close();
105            conn.close();
106        }
107    }
108
109    @Function(schema="javatest", effects=STABLE)
110    public static String getTimeAsString() throws SQLException {
111        ResultSet rs = null;
112        Statement stmt = null;
113        Connection conn = DriverManager
114                .getConnection("jdbc:default:connection");
115        try {
116            stmt = conn.createStatement();
117            rs = stmt.executeQuery("SELECT CURRENT_TIME");
118            if (rs.next())
119                return rs.getTime(1).toString();
120            return "Time could not be retrieved";
121        } finally {
122            if (rs != null)
123                rs.close();
124            if (stmt != null)
125                stmt.close();
126            conn.close();
127        }
128    }
129
130    static void log(String msg) throws SQLException {
131        // GCJ has a somewhat serious bug (reported)
132        //
133        if ("GNU libgcj"
134                .equals(
135                    SessionManager.current().frozenSystemProperties()
136                        .getProperty("java.vm.name"))) {
137            System.out.print("INFO: ");
138            System.out.println(msg);
139        } else
140            Logger.getAnonymousLogger().info(msg);
141    }
142
143    static void warn(String msg) throws SQLException {
144        // GCJ has a somewhat serious bug (reported)
145        //
146        if ("GNU libgcj"
147                .equals(
148                    SessionManager.current().frozenSystemProperties()
149                        .getProperty("java.vm.name"))) {
150            System.out.print("WARNING: ");
151            System.out.println(msg);
152        } else
153            Logger.getAnonymousLogger().warning(msg);
154    }
155
156    @Function(schema="javatest", effects=IMMUTABLE)
157    public static int maxFromSetReturnExample(int base, int increment)
158            throws SQLException {
159        int max = Integer.MIN_VALUE;
160        Connection conn = DriverManager
161                .getConnection("jdbc:default:connection");
162        PreparedStatement stmt = null;
163        ResultSet rs = null;
164
165        try {
166            stmt = conn
167                    .prepareStatement("SELECT base FROM setReturnExample(?, ?)");
168            stmt.setInt(1, base);
169            stmt.setInt(2, increment);
170            rs = stmt.executeQuery();
171            while (rs.next()) {
172                base = rs.getInt(1);
173                if (base > max)
174                    max = base;
175            }
176            return base;
177        } finally {
178            if (rs != null)
179                rs.close();
180            if (stmt != null)
181                stmt.close();
182            conn.close();
183        }
184    }
185
186    /**
187     * Test of bug #1556
188     */
189    @Function(schema="javatest")
190    public static void nestedStatements(int innerCount) throws SQLException {
191        Connection connection = DriverManager
192                .getConnection("jdbc:default:connection");
193        Statement statement = connection.createStatement();
194
195        // Create a set of ID's so that we can do somthing semi-useful during
196        // the long loop.
197        //
198        statement.execute("DELETE FROM javatest.employees1");
199        statement.execute("INSERT INTO javatest.employees1 VALUES("
200                + "1, 'Calvin Forrester', 10000)");
201        statement.execute("INSERT INTO javatest.employees1 VALUES("
202                + "2, 'Edwin Archer', 20000)");
203        statement.execute("INSERT INTO javatest.employees1 VALUES("
204                + "3, 'Rebecka Shawn', 30000)");
205        statement.execute("INSERT INTO javatest.employees1 VALUES("
206                + "4, 'Priscilla Johnson', 25000)");
207
208        int idx = 1;
209        ResultSet results = statement
210                .executeQuery("SELECT * FROM javatest.hugeResult(" + innerCount
211                        + ")");
212        while (results.next()) {
213            Statement innerStatement = connection.createStatement();
214            innerStatement
215                    .executeUpdate("UPDATE javatest.employees1 SET salary = salary + 1 WHERE id="
216                            + idx);
217            innerStatement.close();
218            if (++idx == 5)
219                idx = 0;
220        }
221        results.close();
222        statement.close();
223        connection.close();
224    }
225
226    @SuppressWarnings("removal") // getAttribute / setAttribute
227    private static void nextState(Session session, int expected, int next)
228            throws SQLException {
229        Integer state = (Integer) session.getAttribute(SP_CHECKSTATE);
230        if (state == null || state.intValue() != expected)
231            throw new SQLException(SP_CHECKSTATE + ": Expected " + expected
232                    + ", got " + state);
233        session.setAttribute(SP_CHECKSTATE, next);
234    }
235
236    @Function(schema="javatest", effects=IMMUTABLE)
237    @SuppressWarnings("removal") // setAttribute
238    public static int testSavepointSanity() throws SQLException {
239        Connection conn = DriverManager
240                .getConnection("jdbc:default:connection");
241
242        // Create an anonymous savepoint.
243        //
244        log("Attempting to set an anonymous savepoint");
245        Session currentSession = SessionManager.current();
246        currentSession.setAttribute(SP_CHECKSTATE, 0);
247        currentSession.addSavepointListener(spListener);
248
249        Savepoint sp = conn.setSavepoint();
250        nextState(currentSession, 1, 2);
251        try {
252            Statement stmt = conn.createStatement();
253            log("Attempting to set a SAVEPOINT using SQL (should fail)");
254            stmt.execute("SAVEPOINT foo");
255        } catch (SQLException e) {
256            log("It failed allright. Everything OK then");
257            log("Rolling back to anonymous savepoint");
258
259            nextState(currentSession, 2, 3);
260            conn.rollback(sp);
261            nextState(currentSession, 1, 5);
262            return 1;
263        } finally {
264            currentSession.removeSavepointListener(spListener);
265        }
266        throw new SQLException(
267                "SAVEPOINT through SQL succeeded. That's bad news!");
268    }
269
270    /**
271     * Confirm JDBC behavior of Savepoint, in particular that a Savepoint
272     * rolled back to still exists and can be rolled back to again or released.
273     */
274    @Function(schema="javatest", provides="issue228")
275    public static void issue228() throws SQLException
276    {
277        boolean ok = true;
278        Connection conn =
279            DriverManager.getConnection("jdbc:default:connection");
280        Statement s = conn.createStatement();
281        try
282        {
283            Savepoint alice = conn.setSavepoint("alice");
284            s.execute("SET LOCAL TIME ZONE 1");
285            Savepoint bob   = conn.setSavepoint("bob");
286            s.execute("SET LOCAL TIME ZONE 2");
287            conn.rollback(bob);
288            s.execute("SET LOCAL TIME ZONE 3");
289            conn.releaseSavepoint(bob);
290            try
291            {
292                conn.rollback(bob);
293                ok = false;
294                warn("Savepoint \"bob\" should be invalid after release");
295            }
296            catch ( SQLException e )
297            {
298                if ( ! "3B001".equals(e.getSQLState()) )
299                    throw e;
300            }
301            conn.rollback(alice);
302            bob = conn.setSavepoint("bob");
303            s.execute("SET LOCAL TIME ZONE 4");
304            conn.rollback(alice);
305            try
306            {
307                conn.releaseSavepoint(bob);
308                ok = false;
309                warn(
310                    "Savepoint \"bob\" should be invalid after outer rollback");
311            }
312            catch ( SQLException e )
313            {
314                if ( ! "3B001".equals(e.getSQLState()) )
315                    throw e;
316            }
317            conn.rollback(alice);
318            s.execute("SET LOCAL TIME ZONE 5");
319            conn.releaseSavepoint(alice);
320        }
321        finally
322        {
323            s.close();
324            if ( ok )
325                log("issue 228 tests ok");
326        }
327    }
328
329    @Function(schema="javatest", effects=IMMUTABLE)
330    @SuppressWarnings("removal") // setAttribute
331    public static int testTransactionRecovery() throws SQLException {
332        Connection conn = DriverManager
333                .getConnection("jdbc:default:connection");
334
335        // Create an anonymous savepoint.
336        //
337        log("Attempting to set an anonymous savepoint");
338        Session currentSession = SessionManager.current();
339        currentSession.setAttribute(SP_CHECKSTATE, 0);
340        currentSession.addSavepointListener(spListener);
341
342        Statement stmt = conn.createStatement();
343        Savepoint sp = conn.setSavepoint();
344        nextState(currentSession, 1, 2);
345
346        try {
347            log("Attempting to execute a statement with a syntax error");
348            stmt.execute("THIS MUST BE A SYNTAX ERROR");
349        } catch (SQLException e) {
350            log("It failed. Let's try to recover "
351                    + "by rolling back to anonymous savepoint");
352            nextState(currentSession, 2, 3);
353            conn.rollback(sp);
354            nextState(currentSession, 1, 5);
355            log("Rolled back.");
356            log("Now let's try to execute a correct statement.");
357
358            currentSession.setAttribute(SP_CHECKSTATE, 0);
359            sp = conn.setSavepoint();
360            nextState(currentSession, 1, 2);
361            ResultSet rs = stmt.executeQuery("SELECT 'OK'");
362            while (rs.next()) {
363                log("Expected: OK; Retrieved: " + rs.getString(1));
364            }
365            rs.close();
366            stmt.close();
367            nextState(currentSession, 2, 3);
368            conn.releaseSavepoint(sp);
369            nextState(currentSession, 4, 5);
370            return 1;
371        } finally {
372            currentSession.removeSavepointListener(spListener);
373        }
374
375        // Should never get here
376        return -1;
377    }
378
379    @Function(schema="javatest", name="transferPeople")
380    public static int transferPeopleWithSalary(int salary) throws SQLException {
381        Connection conn = DriverManager
382                .getConnection("jdbc:default:connection");
383        PreparedStatement select = null;
384        PreparedStatement insert = null;
385        PreparedStatement delete = null;
386        ResultSet rs = null;
387
388        String stmt;
389        try {
390            stmt = "SELECT id, name, salary FROM employees1 WHERE salary > ?";
391            log(stmt);
392            select = conn.prepareStatement(stmt);
393
394            stmt = "INSERT INTO employees2(id, name, salary, transferDay, transferTime) VALUES (?, ?, ?, ?, ?)";
395            log(stmt);
396            insert = conn.prepareStatement(stmt);
397
398            stmt = "DELETE FROM employees1 WHERE id = ?";
399            log(stmt);
400            delete = conn.prepareStatement(stmt);
401
402            log("assigning parameter value " + salary);
403            select.setInt(1, salary);
404            log("Executing query");
405            rs = select.executeQuery();
406            int rowNo = 0;
407            log("Doing next");
408            while (rs.next()) {
409                log("Processing row " + ++rowNo);
410                int id = rs.getInt(1);
411                String name = rs.getString(2);
412                int empSal = rs.getInt(3);
413
414                insert.setInt(1, id);
415                insert.setString(2, name);
416                insert.setInt(3, empSal);
417                long now = System.currentTimeMillis();
418                insert.setDate(4, new Date(now));
419                insert.setTime(5, new Time(now));
420                int nRows = insert.executeUpdate();
421                log("Insert processed " + nRows + " rows");
422
423                delete.setInt(1, id);
424                nRows = delete.executeUpdate();
425                log("Delete processed " + nRows + " rows");
426                log("Doing next");
427            }
428            if (rowNo == 0)
429                log("No row found");
430            return rowNo;
431        } finally {
432            if (select != null)
433                select.close();
434            if (insert != null)
435                insert.close();
436            if (delete != null)
437                delete.close();
438            conn.close();
439        }
440    }
441
442    static TransactionListener s_tlstnr;
443
444    public static void registerTransactionListener() throws SQLException
445    {
446        Session currentSession = SessionManager.current();
447        if ( null == s_tlstnr )
448        {
449            s_tlstnr = new XactListener();
450            currentSession.addTransactionListener(s_tlstnr);
451        }
452        else
453        {
454            currentSession.removeTransactionListener(s_tlstnr);
455            s_tlstnr = null;
456        }
457    }
458
459    static class XactListener implements TransactionListener
460    {
461        public void onAbort(Session s)
462        {
463            System.err.println("aborting a transaction");
464        }
465        public void onCommit(Session s)
466        {
467            System.err.println("committing a transaction");
468        }
469        public void onPrepare(Session s)
470        {
471            System.err.println("preparing a transaction");
472        }
473    }
474}