Quartz on Oracle Database

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Oracle Database is one of the databases that Quartz has supported through its JDBC job store from the beginning, and the Quartz jar still contains a delegate class named after it and two DDL scripts for it. A Spring Boot service that already runs on Oracle can therefore keep its triggers, job definitions and cluster state next to its business data. What makes Oracle different from the other SQL pages of this section is not the locking (it is plain SELECT …​ FOR UPDATE, the same statement every other engine receives) but a driver and script combination: since the Oracle JDBC driver moved to the 23 line, the script that Spring Boot picks for Oracle by default stops working, and the symptom is a database error on the very first job that is stored. This page explains why, shows the matrix of what was run, and then goes through the setup in the order in which a reader needs it: dependencies, the delegate, the two scripts, a schema user, migrations, application.yml, locking, clustering and a Testcontainers test. Every statement about what works or fails was produced by running it.

The generic concepts (what the QRTZ_ tables are, initialize-schema, @QuartzDataSource, pool sizing) are on Job Stores & Persistence; the reference database page is Quartz on PostgreSQL; clustering in general is on Clustering. The sibling page with the same template for another engine that needs script edits is Quartz on SQL Server.

It was written against Quartz 2.5.2, Spring Boot 4.1.1, Spring Framework 7.0.9 and Java 21 and verified on 5 October 2026 on an Apple Silicon (arm64) Mac with the Docker image gvenzl/oracle-free:slim-faststart, which started natively on arm64 and whose server reported Oracle AI Database 26ai Free Release 23.26.3.0.0 (version 23.26.3.0.0, the release line that was called Oracle Database 23ai; this page says "23ai" for the BOOLEAN-capable generation and quotes the banner for the exact build) with the pluggable database FREEPDB1. The JDBC driver was ojdbc11 23.26.3.0.0 (the version Spring Boot 4.1.1 manages), and the matrix was repeated with ojdbc11 21.9.0.0 and 23.7.0.25.01 by overriding the Boot property oracle-database.version. The other versions in play were Flyway 12.4.0, Liquibase 5.0.3 (both Boot-managed) and Testcontainers 2.0.5. The projects were compiled for Java 21 and run on JDK 26.0.1. Only Oracle Database Free 23ai was available: a 19c or 21c server was not run, and everything the page says about those releases is labelled as not run. Spring Boot 3.5.16 manages the same Quartz 2.5.2, so everything about Quartz is identical there; only the package of @QuartzDataSource and SchedulerFactoryBeanCustomizer differs (org.springframework.boot.autoconfigure.quartz instead of org.springframework.boot.quartz.autoconfigure), Spring Framework is 6.2.19, and the Boot-managed driver is ojdbc11 23.7.0.25.01, which was also run (it fails the same way as 23.26.3.0.0 with the old script, see the matrix).

Status and dependencies

Status: official. The JDBC job store, the OracleDelegate and the two DDL scripts are part of Quartz itself and are maintained with it, unlike the MongoDB and Redis stores. Quartz’s own tutorial lists Oracle among the databases the JDBC job store "has been used widely with".

Table 1. What you need on Oracle
Item Value measured on this page

Job store

Spring’s LocalDataSourceJobStore (a JobStoreCMT subclass), selected by spring.quartz.job-store-type=jdbc

Delegate

none (Quartz then uses StdJDBCDelegate) or org.quartz.impl.jdbcjobstore.oracle.OracleDelegate; both ran the Bookshelf jobs correctly (see The delegate: what OracleDelegate changes)

Schema script

tables_oracle23.sql on a 23ai server with any driver tried; tables_oracle.sql only together with ojdbc 21.9 or a custom delegate (see Two schema scripts: tables_oracle.sql and tables_oracle23.sql)

Service name in the URL

FREEPDB1 for the Free image; the URL form is jdbc:oracle:thin:@//host:port/service

Clustering

Supported by the JDBC store; measured with two nodes on tables_oracle23.sql and the default delegate (see Clustering)

Testcontainers module

org.testcontainers:testcontainers-oracle-free:2.0.5, class org.testcontainers.oracle.OracleContainer

pom.xml: the dependencies for Quartz on Oracle (versions managed by Spring Boot 4.1.1 unless stated)
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-quartz</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>com.oracle.database.jdbc</groupId>
        <artifactId>ojdbc11</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>testcontainers-oracle-free</artifactId>
        <version>2.0.5</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>testcontainers-junit-jupiter</artifactId>
        <version>2.0.5</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Source: Spring Boot: Quartz Scheduler and Testcontainers: Oracle Free Module. mvn dependency:tree showed ojdbc11 23.26.3.0.0 and Quartz 2.5.2 for this list; the artifact page of the driver is ojdbc11 on Maven Central.

Two details of that list matter. The driver version is not an incidental choice on Oracle: it decides which schema script works, so check it with mvn dependency:tree instead of assuming (Boot 4.1.1 manages 23.26.3.0.0, Boot 3.5.16 manages 23.7.0.25.01). And the Testcontainers 2.x artifact is testcontainers-oracle-free with the container class in the package org.testcontainers.oracle; the 2.0.5 jar contains only that package, no deprecated alias in org.testcontainers.containers.

To pin the driver to an older release, override the Boot property in the POM; this is how the ojdbc11 21.9.0.0 runs on this page were made:

pom.xml: pinning the Oracle driver (Spring Boot’s dependency management reads this property)
<properties>
    <oracle-database.version>21.9.0.0</oracle-database.version>
</properties>

Source: Spring Boot: Dependency Versions. The same override was applied as -Doracle-database.version=21.9.0.0 on the Maven command line for the measurements.

The delegate: what OracleDelegate changes

org.quartz.impl.jdbcjobstore.oracle.OracleDelegate lives in an oracle sub-package, which is the most common way to misspell it: there is no org.quartz.impl.jdbcjobstore.OracleDelegate class in the jar, and a driverDelegateClass without the sub-package fails with a class-not-found error at startup. Its Javadoc calls it "a driver delegate for the Oracle 10 and 11 database". It extends StdJDBCDelegate and overrides the code that writes and reads BLOB columns, nothing else:

Table 2. What OracleDelegate overrides (read from the 2.5.2 source)
Method What it does differently from StdJDBCDelegate

insertJobDetail, updateJobDetail, updateJobData

Inserts or updates the row with EMPTY_BLOB() in JOB_DATA, selects that row with SELECT …​ FOR UPDATE, writes the serialised JobDataMap into the returned locator and runs a second UPDATE with setBlob. The standard delegate binds the bytes in one statement with PreparedStatement.setBytes.

insertTrigger, updateTrigger

The same three-step write for the trigger’s own JOB_DATA (only when the trigger has data; updateTrigger skips it when the map is not dirty).

insertCalendar, updateCalendar

The same for the CALENDAR column of QRTZ_CALENDARS.

writeDataToBlob

Casts the locator to oracle.sql.BLOB, calls putBytes and trim, and throws SQLException("Driver’s Blob representation is of an unsupported type: …​") for any other Blob implementation.

getObjectFromBlob, getJobDataFromBlob

Reads with ResultSet.getBinaryStream; with useProperties=true it returns the stream itself so the properties loader reads it.

The class also depends on Oracle’s own oracle.sql.BLOB, so it needs the Oracle driver on the classpath to load, and it is the reason the delegate is tied to Oracle’s driver and not to any wrapper that hands out another Blob type. The sibling oracle.weblogic.WebLogicOracleDelegate exists for Oracle drivers used inside a WebLogic server; the Quartz configuration page lists it as org.quartz.impl.jdbcjobstore.oracle.WebLogicOracleDelegate, which is not in the jar (the class is in the oracle.weblogic sub-package).

What matters in practice is that the extra machinery is no longer needed with a current driver. The Bookshelf jobs were run twice on Oracle Database Free with ojdbc11 23.26.3.0.0, once with no driverDelegateClass (the scheduler then reports no delegate class and Quartz uses StdJDBCDelegate) and once with OracleDelegate, and a second test stored data of hundreds of kilobytes:

Table 3. A 200 KB JobDataMap, a 50 KB trigger map and a calendar, measured with both delegates
Check StdJDBCDelegate (default) OracleDelegate

Job data byte[200000] read back, compared byte by byte

equal, DBMS_LOB.GETLENGTH(JOB_DATA) = 200306

equal, 200306

Trigger data byte[50000]

read back, 50000 bytes

read back, 50000 bytes

Calendar (HolidayCalendar) stored with addCalendar and read back

found

found

Job replaced with addJob(job, true) and a 10-byte map

10 bytes read back

10 bytes read back

Job described with the empty string ""

read back as null

read back as null

Source: Quartz documentation: Configure JDBCJobStoreTX. The scenario is a test run (store, read back, compare) against a schema created from tables_oracle23.sql; the last row is an Oracle fact, not a Quartz one: Oracle stores an empty string as NULL.

The recommendation that follows is conservative. Leaving the delegate unset is what was tested most (it is also what Boot does when you set nothing), and OracleDelegate is accepted and worked in every run where the schema was right. Choose it if you want the vendor-specific BLOB path that the Quartz documentation names for Oracle, and keep an eye on it when you upgrade the driver, because it casts to a class (oracle.sql.BLOB) that is marked deprecated in the driver; leaving the delegate unset removes that dependency. The setting for the OracleDelegate through Spring Boot is the spring.quartz.properties key shown in application.yml.

Two schema scripts: tables_oracle.sql and tables_oracle23.sql

The 2.5.2 jar carries two Oracle scripts under org/quartz/impl/jdbcjobstore/, and the 2.3.2 and 2.4.1 jars carry only the first one (read from the jar listings). They differ in one thing, the type of the boolean flags:

Table 4. The only differences between the two scripts (diff tables_oracle.sql tables_oracle23.sql)
Column tables_oracle.sql tables_oracle23.sql

QRTZ_JOB_DETAILS.IS_DURABLE, IS_NONCONCURRENT, IS_UPDATE_DATA, REQUESTS_RECOVERY

VARCHAR2(1) NOT NULL

BOOLEAN NOT NULL

QRTZ_SIMPROP_TRIGGERS.BOOL_PROP_1, BOOL_PROP_2

VARCHAR2(1) NULL

BOOLEAN NULL

QRTZ_FIRED_TRIGGERS.IS_NONCONCURRENT, REQUESTS_RECOVERY

VARCHAR2(1) NULL

BOOLEAN NULL

Source: tables_oracle23.sql (v2.5.2) and tables_oracle.sql (v2.5.2); the table is the output of diff on the two files from the jar, plus a trailing blank line that only the old script has.

Everything else is shared: eleven CREATE TABLE statements and twenty CREATE INDEX statements (counted in the file and confirmed with user_tables and user_indexes after a run), VARCHAR2 names of 120 characters for the scheduler name and 200 for the job and trigger names, NUMBER(13) columns for every time (epoch milliseconds, so the database time zone never matters), BLOB columns for the job data, the trigger data, the calendars and the blob triggers, and unquoted lower-case identifiers, which Oracle folds to upper case. The script’s comment says Oracle "MUST be created as shared" with job_queue_processes above 2 and then immediately says other users have run in dedicated mode; the hint dates from the first Quartz releases and it is not needed for the JDBC job store as measured here (the Free image ran in its default mode).

Which script, driver and delegate: the measured matrix

Boot maps the detected database to a script name and uses every id except a handful unchanged: for Oracle that is oracle, so with nothing configured Spring Boot 4.1.1 runs tables_oracle.sql. That default is the one that breaks. The matrix below was produced by one scenario on every row: start the Bookshelf application on an empty schema with initialize-schema=always, check that the cache refresh and the nightly reminders fire, schedule a dynamic job with a serialised JobDataMap, restart with initialize-schema=never and check that the stored state survived.

Table 5. Oracle Database Free 23.26.3.0.0, Boot 4.1.1 and Quartz 2.5.2 (every row was run in this session)
Script Delegate continue-on-error ojdbc11 23.26.3.0.0 ojdbc11 23.7.0.25.01 ojdbc11 21.9.0.0

tables_oracle.sql

default

false

fails at statement 1: ORA-00942 (delete from qrtz_fired_triggers)

same

same

tables_oracle23.sql

default

false

fails at statement 1: ORA-00942

same

same

tables_oracle.sql

default

true (Boot default)

fails at the first job: ORA-12899 on IS_DURABLE (actual 4, maximum 1)

same ORA-12899

works

tables_oracle.sql

OracleDelegate

true

ORA-12899

ORA-12899

works

tables_oracle23.sql

default

true

works

works

works

tables_oracle23.sql

OracleDelegate

true

works

works

works

Source: Spring Boot: Quartz Scheduler and the source of StdJDBCDelegate.setBoolean. The matrix is the result of one test run per driver/database row, with the driver version overridden through the oracle-database.version Maven property for the two older drivers; the rows with false fail before any Quartz code runs, see The leading delete and drop statements.

Three conclusions follow from the matrix.

  • The ORA-12899 is a driver behaviour, not a Quartz bug and not an Oracle delegate bug. StdJDBCDelegate.setBoolean calls PreparedStatement.setBoolean, and the 23.x driver sends a four-character value for it (the error says "actual: 4"), which does not fit into a VARCHAR2(1) column. The 21.9 driver stores 1 and 0 instead (the stock delegate left 1 and 0 in the VARCHAR2(1) columns when it was run, and the BOOLEAN columns of tables_oracle23.sql also accept it). OracleDelegate does not change this, because it calls the same setBoolean.

  • tables_oracle23.sql worked with all three drivers on the 23ai server, so on a 23ai database it is the script to use whatever the driver, and it removes the dependency on the driver’s boolean binding.

  • The BOOLEAN SQL column type exists from Oracle Database 23ai. A 19c or 21c server cannot create tables_oracle23.sql; that was not run here because no such server was available. For those releases the two options below keep VARCHAR2(1) columns and make the driver store 1 and 0.

Decision flow for the Oracle script, driver and delegate. The first question is whether the schema lives on Oracle Database 23ai or later. If yes, the second question is which driver and script. ojdbc 23.x with tables_oracle23.sql and either StdJDBCDelegate or OracleDelegate works, measured. ojdbc 23.x with tables_oracle.sql fails on the first job with ORA-12899 for both delegates. ojdbc 21.9 pinned with tables_oracle.sql works with either delegate, measured on a 23ai server, because only the 23.x driver changed what setBoolean binds. If the server is older than 23ai there is no BOOLEAN column type, so the flags stay VARCHAR2(1) and the driver must store them as text 1 and 0, either by pinning ojdbc 21.x or by keeping ojdbc 23.x and subclassing OracleDelegate to bind with setString; both were measured only against a 23ai server and neither was run on 19c or 21c. A bottom bar notes that every Oracle script starts with delete from and drop table statements that fail on an empty schema with ORA-00942, hidden by Boot only while continue-on-error is true, so Flyway, Liquibase or continue-on-error false need those lines removed
Figure 1. Decision flow: Oracle script, driver and delegate (original figure, from the measured matrix)

Choosing the script in Spring Boot

Boot’s spring.quartz.jdbc.schema property names the script, and spring.quartz.jdbc.platform is the value that replaces @@platform@@ in its default path. Both ways of selecting the 23 script were run and created the tables:

application.yml: two equivalent ways to select tables_oracle23.sql
spring:
  quartz:
    jdbc:
      # either name the script ...
      schema: classpath:org/quartz/impl/jdbcjobstore/tables_oracle23.sql
      # ... or change the platform id that Boot substitutes into tables_@@platform@@.sql
      # platform: oracle23

Source: Spring Boot: Quartz Scheduler. Each variant was run alone (schema, and platform: oracle23 with no schema), with initialize-schema=always, and both passed the Bookshelf scenario with ojdbc11 23.26.3.0.0.

Keeping tables_oracle.sql: pin the driver or bind the flags yourself

If the server is older than 23ai the VARCHAR2(1) script is the only option, and with a 23.x driver the flags must be bound as text. Option A is to pin ojdbc11 to a 21.x release (the property override shown above); with ojdbc11 21.9.0.0 and tables_oracle.sql the stock delegate and OracleDelegate both worked, against the 23ai server. Option B keeps the 23.x driver and overrides the two protected methods that StdJDBCDelegate uses for booleans (setBoolean for writing, and the two getBoolean overloads for reading):

Varchar2FlagOracleDelegate.java: store the flags as '1' and '0' in VARCHAR2(1) columns
package bookshelf;

import org.quartz.impl.jdbcjobstore.oracle.OracleDelegate;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class Varchar2FlagOracleDelegate extends OracleDelegate {

    @Override
    protected void setBoolean(PreparedStatement ps, int index, boolean val) throws SQLException {
        ps.setString(index, val ? "1" : "0");
    }

    @Override
    protected boolean getBoolean(ResultSet rs, String columnName) throws SQLException {
        return "1".equals(rs.getString(columnName));
    }

    @Override
    protected boolean getBoolean(ResultSet rs, int columnIndex) throws SQLException {
        return "1".equals(rs.getString(columnIndex));
    }
}

Source: Quartz Javadoc: StdJDBCDelegate and StdJDBCDelegate.java (v2.5.2). The class compiled and was run as driverDelegateClass against tables_oracle.sql (without its leading statements) with ojdbc11 23.26.3.0.0 and with 21.9.0.0 on the 23ai server: the Bookshelf scenario passed and the IS_DURABLE column held 1 for the four stored jobs. It was not run on a 19c or 21c server, so test it there before relying on it.

Every Oracle script starts with eleven delete from qrtz_…​ statements and eleven drop table qrtz_…​ statements, with no IF EXISTS, before the first CREATE TABLE. On a schema that has no Quartz tables yet each of them fails with ORA-00942 (table or view does not exist). That has three consequences, each measured:

  • With Boot’s default continue-on-error: true the initializer logs the failures, skips them and runs the CREATE statements, so the first start works. The failure is silent, which is how the ORA-12899 of the previous section is the first error a reader sees.

  • With continue-on-error: false startup fails at statement 1, whichever script and driver (the first two rows of the matrix).

  • With initialize-schema: always the head also does its job on an existing schema, and that is a data-loss trap: the application was started with the development profile of this page, stored jobs, restarted, and the runs counter that the nightly job keeps in its JobDataMap had gone from 4 back to 1, because the second start deleted and recreated every table. Never use always against a database whose triggers matter.

A migration tool must not receive those lines: Flyway and Liquibase stop at the first failing statement, and a migration is supposed to run exactly once on an empty schema. The edit is to delete them and keep every CREATE:

One command: tables_oracle23.sql from the jar without the delete and drop statements
unzip -p quartz-2.5.2.jar org/quartz/impl/jdbcjobstore/tables_oracle23.sql \
  | sed -e '/^delete from /d' -e '/^drop table /d' > V1__quartz_tables.sql

Source: tables_oracle23.sql (v2.5.2). The result has 11 CREATE TABLE and 20 CREATE INDEX statements and is byte-identical to the file used in every Flyway, Liquibase and strict (continue-on-error: false) run below.

The same sed on tables_oracle.sql gives the VARCHAR2 variant. Both were run through Boot’s initializer with continue-on-error: false as the schema user QUARTZ described next, and the Bookshelf scenario passed with the 23 script on ojdbc11 23.26.3.0.0, and with the old script on ojdbc11 21.9.0.0 (and failed with ORA-12899 on 23.26.3.0.0, as the matrix says).

A schema user on a pluggable database

Quartz needs a schema of its own, or at least its own set of tables: a user that owns the eleven tables is the simplest arrangement. The Free image is a container database whose application data lives in the pluggable database FREEPDB1, so the connection uses that service name, not the container root. The commands were run as SYSTEM against FREEPDB1:

create-quartz-user.sql: a user that can connect and create its own tables, nothing else
CREATE USER quartz IDENTIFIED BY "<choose-a-strong-password>"
  DEFAULT TABLESPACE users
  TEMPORARY TABLESPACE temp
  QUOTA UNLIMITED ON users;

GRANT CREATE SESSION, CREATE TABLE TO quartz;

Source: Oracle Database SQL Language Reference: CREATE USER. Executed through JDBC against jdbc:oracle:thin:@//localhost:<mapped-port>/FREEPDB1 as SYSTEM; a test password that is not shown here was used. Querying session_privs as the new user returned exactly CREATE SESSION and CREATE TABLE.

The user created that way ran the whole Bookshelf scenario: Boot’s initializer created the eleven tables and the twenty indexes in the QUARTZ schema, Quartz fired the jobs and stored a serialised JobDataMap, and Flyway and Liquibase (next sections) used the same user to create their own bookkeeping tables. No privilege was granted on any other schema and no role was needed. The quota is what lets the user store the table, index and BLOB segments; the CREATE TABLE privilege also covers the indexes the owner creates in its own schema. In production, run the creation script as an administrator once, keep the password in a secret store, and give the application only ${DB_PASSWORD} (see application.yml).

The convenient alternative in tests is to let the Testcontainers module create the user: new OracleContainer(image).withUsername("quartz").withPassword("…​") makes the image create an application user with that name, and querying it showed far more than Quartz needs: 29 session privileges (including CREATE TABLE, CREATE VIEW, CREATE PROCEDURE, CREATE JOB and CREATE MATERIALIZED VIEW) and the roles DB_DEVELOPER_ROLE, RESOURCE and SODA_APP. That is fine for a throw-away container and wrong as a model for a real database.

Two Oracle facts worth keeping next to the setup. If the Quartz tables must live in another schema than the connecting user’s, Quartz substitutes org.quartz.jobStore.tablePrefix verbatim into every statement (the default is QRTZ_), so a schema-qualified prefix such as QUARTZ.QRTZ_ is the mechanism; that variant was not run here. And because Oracle treats an empty string as NULL, a job described with "" reads back with a null description (measured above), which is harmless but shows up in comparisons.

Creating the tables: three ways

Boot’s initializer, for development and tests

Boot’s initializer is enough for a laptop or a test, provided it uses the right script and always is understood to destroy the data. The profile below was run exactly as shown (with the container’s mapped port in place of localhost:1521, and the password supplied as the system property DB_PASSWORD):

application-oracle-dev.yml: development only
spring:
  datasource:
    url: jdbc:oracle:thin:@//localhost:1521/FREEPDB1
    username: quartz
    password: ${DB_PASSWORD}
  quartz:
    job-store-type: jdbc
    jdbc:
      initialize-schema: always         # development and tests only: the script drops the tables first
      schema: classpath:org/quartz/impl/jdbcjobstore/tables_oracle23.sql
      continue-on-error: true           # the default, needed because the script's first statements fail on an empty schema

Source: Spring Boot: Quartz Scheduler. Run with ojdbc11 23.26.3.0.0: the jobs fired. The restart check of the harness then failed, as it should: the second start ran the script again and the stored counter went back from 4 to 1, which is the always behaviour described in The leading delete and drop statements.

Flyway

In production the schema should be created by a migration tool, run once and recorded. With Flyway the production edit is the sed of the previous section and a file name that follows Flyway’s convention. Flyway ships database support as separate modules since version 10, so Oracle needs flyway-database-oracle next to the Boot starter; both are managed by Boot 4.1.1 (12.4.0 was run).

pom.xml: Flyway with Oracle support
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-flyway</artifactId>
</dependency>
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-database-oracle</artifactId>
</dependency>
src/main/resources/db/migration/oracle/V1__quartz_tables.sql: the script from the previous section
-- the 11 CREATE TABLE and 20 CREATE INDEX statements of tables_oracle23.sql,
-- without its leading "delete from" and "drop table" statements
CREATE TABLE qrtz_job_details
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    JOB_NAME  VARCHAR2(200) NOT NULL,
    JOB_GROUP VARCHAR2(200) NOT NULL,
    DESCRIPTION VARCHAR2(250) NULL,
    JOB_CLASS_NAME   VARCHAR2(250) NOT NULL,
    IS_DURABLE BOOLEAN NOT NULL,
    IS_NONCONCURRENT BOOLEAN NOT NULL,
    IS_UPDATE_DATA BOOLEAN NOT NULL,
    REQUESTS_RECOVERY BOOLEAN NOT NULL,
    JOB_DATA BLOB NULL,
    CONSTRAINT QRTZ_JOB_DETAILS_PK PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
);
-- ... the other ten tables and the indexes, exactly as in the jar script

Source: tables_oracle23.sql (v2.5.2). Only the first table is shown; the file that Flyway ran is the complete output of the sed command above.

application-oracle-flyway.yml: Flyway creates the tables, Quartz only uses them
spring:
  datasource:
    url: jdbc:oracle:thin:@//localhost:1521/FREEPDB1
    username: quartz
    password: ${DB_PASSWORD}
  quartz:
    job-store-type: jdbc
    scheduler-name: bookshelf
    jdbc:
      initialize-schema: never          # the schema is owned by a migration tool, see below
    properties:
      org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.oracle.OracleDelegate
      org.quartz.jobStore.misfireThreshold: "60000"
      org.quartz.threadPool.threadCount: "10"
  flyway:
    locations: classpath:db/migration/oracle

Source: Spring Boot: Database Initialization and Spring Boot: Quartz Scheduler. This exact file was run with only Flyway on the classpath, with the container’s URL in place of localhost:1521: Flyway applied V1__quartz_tables.sql, Quartz started afterwards, the Bookshelf jobs fired and the stored counter survived a restart (5 to 6). In the equivalent run that queried it, flyway_schema_history held one row for version 1 with success set.

Boot orders the scheduler after the database initializers, so Flyway runs before Quartz reads its tables. The schema then contained 12 tables, the 11 Quartz tables plus flyway_schema_history (Flyway quotes its name in lower case on Oracle). Do not edit the migration after it was applied; a change to the Quartz tables, for example when upgrading Quartz, is a new V2__ migration. The general workflow is on Schema Evolution and the choice between tools on Choosing a Migration Tool.

Liquibase

Liquibase reads the same edited script through sqlFile. The changelog below, with the script next to it, created the 11 Quartz tables plus Liquibase’s DATABASECHANGELOG and DATABASECHANGELOGLOCK (13 tables in all) and the Bookshelf jobs ran afterwards.

src/main/resources/db/changelog/oracle/db.changelog-master.yaml: the Quartz tables as one changeset
databaseChangeLog:
  - changeSet:
      id: quartz-tables-oracle23
      author: bookshelf
      changes:
        - sqlFile:
            path: db/changelog/oracle/quartz/tables_oracle23.sql
            splitStatements: true
            endDelimiter: ;
            stripComments: true

Source: Liquibase: sqlFile and Spring Boot: Using Liquibase. The file db/changelog/oracle/quartz/tables_oracle23.sql is the output of the sed command above; the changelog ran under Liquibase 5.0.3.

application-oracle-liquibase.yml: point Boot at the changelog
spring:
  datasource:
    url: jdbc:oracle:thin:@//localhost:1521/FREEPDB1
    username: quartz
    password: ${DB_PASSWORD}
  quartz:
    job-store-type: jdbc
    scheduler-name: bookshelf
    jdbc:
      initialize-schema: never          # the schema is owned by a migration tool
  liquibase:
    change-log: classpath:db/changelog/oracle/db.changelog-master.yaml

Source: Spring Boot: Database Initialization. Run exactly as shown (container URL substituted) with only Liquibase on the classpath; the Bookshelf jobs fired and the stored counter survived a restart (6 to 7).

The Quartz jar also contains a database-neutral changelog, org/quartz/impl/jdbcjobstore/liquibase.quartz.init.xml (it is in the Quartz jar, not in Spring Boot): one changeset, quartz-init, that creates the same 11 tables with a blob_type property that is BYTEA on PostgreSQL and BLOB elsewhere. It can be included from a master changelog with file: org/quartz/impl/jdbcjobstore/liquibase.quartz.init.xml, and it was run on the 23ai server with ojdbc11 23.26.3.0.0 and the default delegate: it works, and the Oracle catalogue afterwards showed BOOLEAN for IS_DURABLE and BOOL_PROP_1 and BLOB for JOB_DATA, CALENDAR and BLOB_DATA. That is the same column shape as tables_oracle23.sql, so on a 23ai database it is the shortest route. How Liquibase maps its BOOLEAN type on an older Oracle release was not run here. Choose the bundled file when you want one changelog for every database in the fleet, and the Oracle script when you want exactly the DDL that the Oracle page of Quartz ships; do not mix them in one environment. The general Liquibase workflow is on Liquibase: Getting Started.

Source: liquibase.quartz.init.xml (v2.5.2). The column types were read from user_tab_columns of a schema created by this file.

application.yml

The Flyway profile above is the shape of a production configuration; this is the same configuration with the variants spelled out, in the form that a deployment would use, with every secret and host name coming from the environment:

application.yml: Oracle in production, the schema owned by a migration tool
spring:
  datasource:
    url: jdbc:oracle:thin:@//${DB_HOST}:1521/${DB_SERVICE}   # for the Free image: FREEPDB1
    username: quartz
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 13              # worker threads + 3, see Production & Best Practices
  quartz:
    job-store-type: jdbc
    scheduler-name: bookshelf
    jdbc:
      initialize-schema: never
    properties:
      org.quartz.threadPool.threadCount: "10"
      org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.oracle.OracleDelegate   # optional

Source: Spring Boot: Quartz Scheduler. The keys are the ones of the Flyway profile that was run; the hikari line and the URL placeholders are the only additions, and the pool value follows the rule of Job Stores & Persistence.

A few remarks. The URL uses the service-name form @//host:port/service; the older host:port:SID form names an instance and is the wrong choice for a pluggable database (that form was not run). No driver-class-name is needed, Boot infers oracle.jdbc.OracleDriver from the URL. org.quartz.jobStore.misfireThreshold defaults to 60000 ms on a JDBC store (read from JobStoreSupport), so the line in the Flyway profile only restates the default. And when a second DataSource exists in the application, mark one @Primary (and the Quartz one @QuartzDataSource when it is not the primary); with no @Primary Boot silently falls back to the RAM store, even if one data source is @QuartzDataSource, as explained on the job stores page.

The Bookshelf jobs on Oracle

The five kinds of work of the Bookshelf scenario ran unchanged on Oracle: the cache refresh (a SimpleTrigger), the nightly overdue-loan reminders, the weekly statistics and the monthly retention cleanup (three CronTrigger`s), and a per-member reminder scheduled at runtime with a serialised `JobDataMap. After a run the tables held four jobs and four triggers (three cron, one simple), the dynamic job fired with its data, and after a restart with initialize-schema=never the @PersistJobDataAfterExecution counter of the overdue job carried on (5 to 6 in the Flyway run). While the scheduler is running these queries show what Quartz keeps; the epoch-millisecond columns are converted with Oracle’s NUMTODSINTERVAL:

SQL: what Quartz stores, as seen from SQL*Plus or SQLcl
SELECT trigger_name, trigger_state, trigger_type,
       TO_CHAR(TIMESTAMP '1970-01-01 00:00:00 +00:00' + NUMTODSINTERVAL(next_fire_time / 1000, 'SECOND'),
               'YYYY-MM-DD HH24:MI:SS TZR') AS next_fire
FROM   qrtz_triggers ORDER BY next_fire_time;

SELECT job_name, job_group, is_durable, is_nonconcurrent, requests_recovery,
       DBMS_LOB.GETLENGTH(job_data) AS job_data_bytes
FROM   qrtz_job_details ORDER BY job_name;

SELECT instance_name, checkin_interval FROM qrtz_scheduler_state;
SELECT lock_name FROM qrtz_locks ORDER BY lock_name;

Source: Quartz Tutorial: Lesson 9, JobStores. The statements were run against the schema of a running clustered scheduler.

Table 6. Result of the statements above (clustered scheduler, Bookshelf triggers on their real schedules)
qrtz_triggers: name state type next fire (UTC)

cacheRefreshTrigger

WAITING

SIMPLE

5 minutes after the previous run

overdueLoanRemindersTrigger

WAITING

CRON

2026-10-06 00:00:00 (02:00 in the JVM’s zone, Europe/Madrid, UTC+2 in October)

weeklyStatisticsTrigger

WAITING

CRON

2026-10-12 04:00:00 (06:00 local on Monday)

retentionCleanupTrigger

WAITING

CRON

2026-11-01 02:30:00 (03:30 local, UTC+1 after the clock change)

Source: Quartz Tutorial: Lesson 9, JobStores. The job table showed is_durable as true for all four jobs, is_nonconcurrent as true only for overdueLoanReminders, a JOB_DATA of 273 bytes (357 for the job with a counter), one QRTZ_SCHEDULER_STATE row with checkin_interval 2000 and 532 ms since its last check-in, and the two lock rows. The cron triggers in this run used the default time zone of the JVM, Europe/Madrid, which is why the UTC fire times are offset from the cron fields; a production trigger should carry an explicit TimeZone.

Transactions and locking

Under Spring Boot the job store is LocalDataSourceJobStore, a JobStoreCMT subclass, and JobStoreCMT.initialize forces database locks on unless the data source is HSQL (the Spring class switches to an in-memory semaphore only for HSQL). So on Oracle every Quartz scheduler uses StdRowLockSemaphore, even a single non-clustered node, and a clustered one needs it by definition. The semaphore takes a row lock with exactly this statement (the constant SELECT_FOR_LOCK, with the table prefix and scheduler name filled in):

The lock statement, as Oracle received it (from V$SQL while a clustered scheduler was running)
SELECT * FROM QRTZ_LOCKS WHERE SCHED_NAME = 'BookshelfCluster' AND LOCK_NAME = :1 FOR UPDATE

Source: StdRowLockSemaphore.java (v2.5.2) and Oracle Database SQL Language Reference: SELECT, the FOR UPDATE clause. The statement text and its execution count (9 in an eight second run with a two second check-in) come from V$SQL; the same view also showed the INSERT INTO QRTZ_LOCKS(SCHED_NAME, LOCK_NAME) VALUES (…​) that creates a lock row on first use (twice, once for STATE_ACCESS and once for TRIGGER_ACCESS; the script inserts none).

That is plain Oracle SQL, so no selectWithLockSQL override is needed (unlike SQL Server, where Quartz substitutes lock hints). The two lock names matter: TRIGGER_ACCESS guards every change of trigger state that must not interleave between nodes, and STATE_ACCESS guards the cluster check-in and the recovery of a failed node. By default acquireNextTriggers does not take TRIGGER_ACCESS at all (acquireTriggersWithinLock is false and the batch size is 1): it moves a trigger from WAITING to ACQUIRED with a conditional UPDATE and checks that one row changed, and only the following triggersFired call, which turns the acquired trigger into a fired one, runs inside the lock.

sequenceDiagram participant A as Node A participant DB as Oracle QRTZ tables participant B as Node B A->>DB: UPDATE trigger WAITING to ACQUIRED where state is WAITING B->>DB: same UPDATE for the same trigger DB-->>A: 1 row updated, A owns the trigger DB-->>B: 0 rows updated, B moves on A->>DB: SELECT FOR UPDATE on TRIGGER_ACCESS lock row Note over A,DB: the row stays locked until A commits A->>DB: triggersFired inserts QRTZ_FIRED_TRIGGERS and updates the trigger A->>DB: COMMIT releases the lock B->>DB: SELECT FOR UPDATE on TRIGGER_ACCESS for its own triggersFired Note over B,DB: waits only while another node holds the row

Source: JobStoreSupport.java (v2.5.2), methods acquireNextTriggers and triggersFired. The two statements are drawn from the source; the lock behaviour of the row itself is the measurement below.

Oracle’s behaviour on that row was measured with two JDBC sessions on a schema with a clustered scheduler stopped in standby: the first session ran the statement above and kept its transaction open, and the second session tried to lock the same row:

Table 7. The same row locked from a second session while the first transaction was open
Second session’s statement Result

…​ FOR UPDATE (what Quartz sends)

blocked: still waiting after 2000 ms; it returned TRIGGER_ACCESS right after the first session rolled back (2008 ms after it started)

…​ FOR UPDATE NOWAIT

ORA-00054 after 8 to 14 ms in the two runs

…​ FOR UPDATE WAIT 2

ORA-00054 after about 2 seconds (2009 ms; the same error, raised when the timeout expires)

…​ FOR UPDATE SKIP LOCKED

returned no rows after 4 to 7 ms

Source: Oracle Database Concepts: Data Concurrency and Consistency. The measurements are from a test run with two connections to QRTZ_LOCKS.

Two practical consequences. A session that runs SELECT …​ FOR UPDATE on QRTZ_LOCKS for inspection and is left open in a SQL client freezes the firing of every node until it ends: use plain SELECT statements (as in the queries above, which Oracle answers without locking, because readers do not block writers). And Oracle’s undo-based concurrency means the tables do not need a VACUUM-style routine for the constant insert and delete churn of QRTZ_FIRED_TRIGGERS (the PostgreSQL page has to deal with bloat there). That second point is Oracle’s documented behaviour and was not measured.

The isolation level is the Oracle default. A connection from the Hikari pool reported Connection.TRANSACTION_READ_COMMITTED (2) with auto-commit on; LocalDataSourceJobStore sets dontSetAutoCommitFalse because Spring manages the transaction around the Quartz calls (see Transaction Isolation & Locking for the Spring side). Quartz has an opt-in org.quartz.jobStore.txIsolationLevelSerializable (default false in JobStoreSupport); the Oracle documentation says that a serializable transaction raises an error instead of waiting when it meets a concurrent change, so leave it off on Oracle. That behaviour is documented, not measured here. According to the Oracle documentation the database offers only READ COMMITTED, SERIALIZABLE and read-only transactions, which is why the Quartz option offers no other level. If a lock attempt fails, StdRowLockSemaphore retries up to 3 times one second apart before it throws (maxRetry and retryPeriod, read from the source).

Clustering

Clustering uses the same tables and the same row locks; the setup is the Boot properties of Clustering plus the Oracle ones of this page. It was run with two schedulers in one JVM (same scheduler name, same schema, instanceId AUTO, isClustered=true, clusterCheckinInterval=2000, three worker threads each), on tables_oracle23.sql, the default delegate and ojdbc11 23.26.3.0.0, with the first node creating the schema (always, continue-on-error at its default) and the second using never:

Table 8. Two nodes on Oracle Database Free, 10 seconds, every second and every two seconds a job fires
Job Distinct scheduled fire times Executions Scheduled times executed more than once

cacheRefresh (every 1 s)

14

14 (all on one node)

0

overdueLoanReminders (cron every 2 s)

7

7 (2 on the first node, 5 on the second)

0

Source: Quartz documentation: Configure Clustering with JDBC-JobStore. The numbers are from one run of a two-node test; Quartz does not balance load, a free node takes whatever it acquires first, so the split between nodes varies from run to run.

No scheduled time ran twice and both nodes reported clustered=true. The failure-detection rules (a node is declared dead after its last check-in plus the larger of the check-in interval and its own check-in delay, plus 7500 ms), the kill-and-recover behaviour of requestsRecovery and the three-node case are measured on PostgreSQL and described on Clustering; they are engine-independent and were not repeated on Oracle. Oracle RAC (several database instances) was not available and was not tested. The cluster uses the JVM clock of each node for check-ins, not the database clock, so keep the nodes' clocks synchronised.

A Testcontainers test for the Bookshelf jobs

The test below starts Oracle Database Free in a container, lets Boot create the schema from the headless tables_oracle23.sql with continue-on-error=false (so any script error fails the test; the property exists on Spring Boot 4.x only, on 3.5.16 the initializer hard-codes true), and checks that the Bookshelf triggers are rows in Oracle and that the cache refresh fired and persisted its previous fire time. The image is large (4.81 GB) but started natively on the Apple Silicon Mac that ran the tests: the container was ready in 6 to 9 seconds and the whole test class took 17 seconds.

src/test/java/bookshelf/BookshelfOracleTest.java
package bookshelf;

import org.junit.jupiter.api.Test;
import org.quartz.Scheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.oracle.OracleContainer;

import java.time.Duration;

import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

@SpringBootTest(classes = BookshelfApplication.class, properties = {
        "spring.quartz.job-store-type=jdbc",
        "spring.quartz.jdbc.initialize-schema=always",
        "spring.quartz.jdbc.continue-on-error=false",
        "spring.quartz.jdbc.schema=classpath:db/quartz/tables_oracle23_nohead.sql",
        "spring.quartz.scheduler-name=bookshelf"
})
@Testcontainers
class BookshelfOracleTest {

    @Container
    static final OracleContainer ORACLE = new OracleContainer("gvenzl/oracle-free:slim-faststart")
            .withUsername("quartz")
            .withPassword("quartz-test-only");

    @DynamicPropertySource
    static void datasource(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", ORACLE::getJdbcUrl);
        registry.add("spring.datasource.username", ORACLE::getUsername);
        registry.add("spring.datasource.password", ORACLE::getPassword);
    }

    @Autowired Scheduler scheduler;
    @Autowired JdbcTemplate jdbc;

    @Test
    void storesTheBookshelfJobsInOracleAndFiresThem() throws Exception {
        assertThat(scheduler.getMetaData().isJobStoreSupportsPersistence()).isTrue();
        assertThat(scheduler.getMetaData().getJobStoreClass().getSimpleName()).isEqualTo("LocalDataSourceJobStore");

        // the schema script created the 11 QRTZ_ tables in the QUARTZ schema
        assertThat(jdbc.queryForObject(
                "SELECT COUNT(*) FROM user_tables WHERE table_name LIKE 'QRTZ\\_%' ESCAPE '\\'", Integer.class)).isEqualTo(11);

        // the Bookshelf triggers are rows, not memory
        assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM qrtz_triggers", Integer.class)).isEqualTo(4);

        // the cache refresh fired at least once and its previous fire time was persisted
        await().atMost(Duration.ofSeconds(30)).untilAsserted(() ->
                assertThat(jdbc.queryForObject(
                        "SELECT prev_fire_time FROM qrtz_triggers WHERE trigger_name = 'cacheRefreshTrigger'", Long.class))
                        .isGreaterThan(0L));

        // flags are real booleans in the 23ai script
        assertThat(jdbc.queryForObject(
                "SELECT COUNT(*) FROM qrtz_job_details WHERE is_durable = TRUE", Integer.class)).isEqualTo(4);
    }
}

Source: Testcontainers: Oracle Free Module and Spring Boot: Quartz Scheduler. This class was compiled and run (1 test, 0 failures) in a test run, with src/test/resources/db/quartz/tables_oracle23_nohead.sql being the output of the sed command of The leading delete and drop statements. Its BookshelfApplication and the five Bookshelf jobs are the ones of Getting Started with Spring Boot. The password is a test value that exists only inside the throw-away container.

What was run, precisely: the gvenzl/oracle-free:slim-faststart image (Oracle Database Free, banner Oracle AI Database 26ai Free Release 23.26.3.0.0), testcontainers-oracle-free 2.0.5, ojdbc11 23.26.3.0.0 for this class, and ojdbc11 21.9.0.0 and 23.7.0.25.01 for the matrix scenarios. What matters on Oracle is the user: withUsername creates a broad application user (see A schema user on a pluggable database), so a test that wants to prove that a least-privilege user is enough must create the user itself, as the tests behind this page did with CREATE USER and the two grants. The general technique is on Spring Boot Integration Tests with Testcontainers and the full Quartz testing story on Testing.

Pitfalls

Table 9. Oracle pitfalls
Pitfall What to do

Running the default Boot configuration against Oracle with a 23.x driver

Boot’s default script for Oracle is tables_oracle.sql, and with ojdbc11 23.26.3.0.0 (and 23.7.0.25.01) the first stored job fails with ORA-12899 on IS_DURABLE (actual 4, maximum 1). On a 23ai server use tables_oracle23.sql through spring.quartz.jdbc.schema or platform: oracle23; on an older server pin the driver to 21.x or bind the flags as text (see Two schema scripts: tables_oracle.sql and tables_oracle23.sql).

Expecting OracleDelegate to fix the flag-column error

It calls the same setBoolean as the standard delegate and fails identically with the old script and a 23.x driver (measured). The delegate only changes how the BLOB columns are written.

Misspelling the delegate

The class is org.quartz.impl.jdbcjobstore.oracle.OracleDelegate. There is no org.quartz.impl.jdbcjobstore.OracleDelegate, and the WebLogicOracleDelegate that the configuration page lists in the oracle package is in oracle.weblogic.

The ORA-00942 from the script’s delete from and drop table lines

Silent while continue-on-error is true; fatal with false, with Flyway and with Liquibase. Delete those lines for every migration (the sed command above).

initialize-schema: always on a real database

The script deletes and drops the tables first, so every restart wipes jobs, triggers and counters (a JobDataMap counter went from 4 back to 1 in the Oracle run). Use it for tests only.

Reading the ORA-12899 as a Quartz bug and downgrading Quartz

The cause is the driver’s setBoolean against VARCHAR2(1); Quartz 2.3.2 and 2.4.1 ship only the old script. Change the script (or the driver), not the Quartz version.

BOOLEAN columns on an older server

tables_oracle23.sql needs the BOOLEAN column type of 23ai. A 19c or 21c server needs tables_oracle.sql with a driver or delegate that stores 1 and 0; that combination was run only against a 23ai server.

Connecting with the wrong service name

Use the service of the pluggable database in the form @//host:port/service (FREEPDB1 for the Free image). The Testcontainers getJdbcUrl() already returns a URL with that service (jdbc:oracle:thin:@localhost:<port>/freepdb1 was measured). A SID-style URL (host:port:SID) was not run.

Giving the application the powerful user of a test container

withUsername creates a user whose session holds 29 system privileges (directly or through the roles RESOURCE, DB_DEVELOPER_ROLE and SODA_APP). The Quartz schema owner ran with CREATE SESSION and CREATE TABLE plus a quota.

Leaving SELECT …​ FOR UPDATE open on QRTZ_LOCKS in a SQL client

The row lock blocks every node’s triggersFired until the session ends (measured: a second FOR UPDATE stayed blocked). Inspect with plain SELECT.

Enabling txIsolationLevelSerializable to be safe

Oracle’s serializable mode raises an error on concurrent changes instead of waiting (Oracle documentation, not measured here). The default READ COMMITTED plus the row locks is what Quartz expects.

Comparing descriptions with ""

Oracle stores the empty string as NULL (measured: a job described with "" reads back null). Treat null and empty as the same.

Writing directly to the QRTZ_ tables from SQL clients

Quartz keeps cross-table invariants (trigger state, fired triggers, lock rows) that a manual UPDATE breaks; use the Scheduler API, and see the production page for the rule.

OracleDelegate behind a pool or proxy that wraps Blob

writeDataToBlob accepts only oracle.sql.BLOB and throws "Driver’s Blob representation is of an unsupported type" otherwise (read from the source; not provoked here). The default delegate has no such cast.

Assuming the Free image tells you its version

The image has no version label; query v$version (Oracle AI Database 26ai Free Release 23.26.3.0.0 for the image used here) and pin the tag in your tests.

What changed

  • Quartz 2.5.2’s jar adds tables_oracle23.sql (BOOLEAN flags, Oracle 23ai). The jars of Quartz 2.3.2 and 2.4.1 contain only tables_oracle.sql; OracleDelegate and WebLogicOracleDelegate are in all three. The jar grows from 22 DDL scripts to 25 overall, as listed on the job stores page.

  • Spring Boot 4.1.1 manages ojdbc11 23.26.3.0.0; Boot 3.5.16 manages 23.7.0.25.01. Under Boot 4.1.1 both versions fail on tables_oracle.sql (measured by overriding oracle-database.version); the Boot 3.5.16 build itself was not run. The Boot 3.5 property names, including spring.quartz.jdbc.*, are unchanged in Boot 4; the types moved to org.springframework.boot.quartz.autoconfigure.

  • Boot still maps Oracle to the unchanged platform id oracle, so the default script is the old one; it does not select the 23 script by itself.

  • Testcontainers 2.x moved the Oracle module to testcontainers-oracle-free and org.testcontainers.oracle.OracleContainer.

  • The Quartz configuration page still lists a WebLogicOracleDelegate in the oracle package, which the 2.5.2 jar does not contain; the class is in oracle.weblogic. This is collected with the other documentation discrepancies on Migration & What’s Changed.

Where to go next