diff --git a/docs/sphinx/requirements.txt b/docs/sphinx/requirements.txt index d24bf5d264..b580e4d055 100644 --- a/docs/sphinx/requirements.txt +++ b/docs/sphinx/requirements.txt @@ -18,6 +18,7 @@ snowballstemmer==2.2.0 soupsieve==2.5 sphinx==8.1.3 sphinx-basic-ng==1.0.0b2 +sphinx-design==0.6.1 sphinxcontrib-devhelp==2.0.0 sphinxcontrib-htmlhelp==2.1.0 sphinxcontrib-jsmath==1.0.1 diff --git a/docs/sphinx/source/SQL_Reference.md b/docs/sphinx/source/SQL_Reference.md index f22ead7db0..9eaaf69cd0 100644 --- a/docs/sphinx/source/SQL_Reference.md +++ b/docs/sphinx/source/SQL_Reference.md @@ -26,6 +26,5 @@ reference/Indexes :caption: Miscellaneous :maxdepth: 2 -reference/Direct_Access_Api reference/understanding_bitmap ``` diff --git a/docs/sphinx/source/conf.py b/docs/sphinx/source/conf.py index c4424af20b..123dff82b0 100644 --- a/docs/sphinx/source/conf.py +++ b/docs/sphinx/source/conf.py @@ -30,7 +30,7 @@ copyright = '2025, Apple Inc.' author = 'Apple Inc.' -extensions = ['myst_parser'] +extensions = ['myst_parser', 'sphinx_design'] templates_path = ['_templates'] exclude_patterns = [] @@ -66,4 +66,5 @@ """ myst_heading_anchors = 4 +myst_enable_extensions = ["colon_fence"] diff --git a/docs/sphinx/source/index.md b/docs/sphinx/source/index.md index 0f5d994605..c2e5d9ec17 100644 --- a/docs/sphinx/source/index.md +++ b/docs/sphinx/source/index.md @@ -26,6 +26,7 @@ reliability, and performance in a distributed setting. :maxdepth: 1 Overview and Examples GettingStarted +JDBC Guide Building SchemaEvolution Extending diff --git a/docs/sphinx/source/jdbc/advanced.rst b/docs/sphinx/source/jdbc/advanced.rst new file mode 100644 index 0000000000..ca89cbf3da --- /dev/null +++ b/docs/sphinx/source/jdbc/advanced.rst @@ -0,0 +1,224 @@ +====================== +Advanced JDBC Features +====================== + +.. important:: + The JDBC interface is experimental and not production-ready at this stage. APIs and behaviors are subject to change. + +This guide covers FoundationDB Record Layer-specific JDBC features for working with complex data types like STRUCTs and ARRAYs. + +.. note:: + **Driver-Specific Classes**: When creating STRUCT and ARRAY values, use the appropriate builder for your JDBC driver: + + - **Embedded Driver**: ``EmbeddedRelationalStruct`` and ``EmbeddedRelationalArray`` (from ``com.apple.foundationdb.relational.api``) + - **Server Driver**: ``JDBCRelationalStruct`` and ``JDBCRelationalArray`` (from ``com.apple.foundationdb.relational.jdbc``) + + Both provide identical APIs, so the code examples in this guide work with either by changing the import and class name. + +Working with STRUCT Types +========================== + +The Record Layer extends standard JDBC to support STRUCT types, which represent nested record structures similar to protobuf messages. STRUCTs allow you to model hierarchical data within your relational schema. + +Reading STRUCT Values +--------------------- + +To read STRUCT values from a query result, unwrap the ``ResultSet`` to ``RelationalResultSet`` which provides direct access to STRUCTs: + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/AdvancedSnippets.java + :language: java + :start-after: // tag::read-struct[] + :end-before: // end::read-struct[] + :dedent: 8 + +Accessing Nested STRUCT Fields +------------------------------- + +STRUCTs can contain other STRUCTs, allowing for deeply nested data structures. Here's how to read a STRUCT with a nested STRUCT: + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/ComplexTypesEmbedded.java + :language: java + :start-after: // tag::query-struct[] + :end-before: // end::query-struct[] + :dedent: 4 + :lines: 1-26 + +Creating STRUCT Values +---------------------- + +Use the appropriate builder depending on your JDBC driver. Both builders provide identical APIs. Here's an example inserting a STRUCT into a STRUCT column: + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/AdvancedSnippets.java + :language: java + :start-after: // tag::create-struct-simple[] + :end-before: // end::create-struct-simple[] + :dedent: 8 + +Creating Nested STRUCTs +----------------------- + +You can create STRUCTs that contain nested STRUCTs and insert them as a single value. Here's an example that creates a customer STRUCT with a nested address STRUCT: + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/ComplexTypesEmbedded.java + :language: java + :start-after: // tag::insert-struct[] + :end-before: // end::insert-struct[] + :dedent: 4 + :lines: 1-20 + +Handling NULL STRUCT Fields +---------------------------- + +You can set NULL values for individual fields within a STRUCT: + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/AdvancedSnippets.java + :language: java + :start-after: // tag::create-struct-null[] + :end-before: // end::create-struct-null[] + :dedent: 8 + +Working with ARRAY Types +========================= + +The Record Layer supports ARRAY types containing elements of any SQL type, including primitives, STRUCTs, and even nested ARRAYs. + +Reading ARRAY Values +-------------------- + +Use the ``getArray()`` method to retrieve ARRAY values from a ``ResultSet``: + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/AdvancedSnippets.java + :language: java + :start-after: // tag::read-array-basic[] + :end-before: // end::read-array-basic[] + :dedent: 8 + +Using ResultSet to Iterate Arrays +---------------------------------- + +You can also retrieve array elements as a ``ResultSet`` for more structured access: + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/AdvancedSnippets.java + :language: java + :start-after: // tag::read-array-resultset[] + :end-before: // end::read-array-resultset[] + :dedent: 8 + +Creating ARRAY Values +--------------------- + +Use the appropriate builder depending on your JDBC driver. Both builders provide identical APIs: + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/AdvancedSnippets.java + :language: java + :start-after: // tag::create-array-basic[] + :end-before: // end::create-array-basic[] + :dedent: 8 + +Working with Different Array Element Types +------------------------------------------- + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/AdvancedSnippets.java + :language: java + :start-after: // tag::create-array-types[] + :end-before: // end::create-array-types[] + :dedent: 8 + +Handling NULL Arrays +-------------------- + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/AdvancedSnippets.java + :language: java + :start-after: // tag::array-null[] + :end-before: // end::array-null[] + :dedent: 8 + +Complex Nested Types +==================== + +Arrays of STRUCTs +----------------- + +ARRAYs can contain STRUCT elements, allowing for collections of complex records: + +.. tab-set:: + + .. tab-item:: Embedded Driver + :sync: embedded + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/AdvancedSnippets.java + :language: java + :start-after: // tag::array-of-structs[] + :end-before: // end::array-of-structs[] + :dedent: 8 + + .. tab-item:: Server Driver + :sync: server + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/AdvancedSnippetsServer.java + :language: java + :start-after: // tag::array-of-structs[] + :end-before: // end::array-of-structs[] + :dedent: 8 + +Reading Arrays of STRUCTs +------------------------- + +When reading arrays that contain STRUCT elements, unwrap the array's ``ResultSet`` to ``RelationalResultSet``: + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/ComplexTypesEmbedded.java + :language: java + :start-after: // tag::query-struct[] + :end-before: // end::query-struct[] + :dedent: 4 + :lines: 28-47 + +STRUCTs Containing Arrays +------------------------- + +STRUCTs can contain ARRAY fields. You can insert the entire STRUCT including its arrays as a single value: + +.. tab-set:: + + .. tab-item:: Embedded Driver + :sync: embedded + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/AdvancedSnippets.java + :language: java + :start-after: // tag::struct-containing-arrays[] + :end-before: // end::struct-containing-arrays[] + :dedent: 8 + + .. tab-item:: Server Driver + :sync: server + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/AdvancedSnippetsServer.java + :language: java + :start-after: // tag::struct-containing-arrays[] + :end-before: // end::struct-containing-arrays[] + :dedent: 8 + +Inspecting Array Metadata +========================== + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/AdvancedSnippets.java + :language: java + :start-after: // tag::array-metadata[] + :end-before: // end::array-metadata[] + :dedent: 8 + +Inspecting STRUCT Metadata +=========================== + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/AdvancedSnippets.java + :language: java + :start-after: // tag::struct-metadata[] + :end-before: // end::struct-metadata[] + :dedent: 8 + +See Also +======== + +- :doc:`basic` - Basic JDBC usage patterns +- :doc:`../SQL_Reference` - Complete SQL syntax reference +- :doc:`../reference/Databases_Schemas_SchemaTemplates` - Understanding the data model diff --git a/docs/sphinx/source/jdbc/basic.rst b/docs/sphinx/source/jdbc/basic.rst new file mode 100644 index 0000000000..3e814c69d9 --- /dev/null +++ b/docs/sphinx/source/jdbc/basic.rst @@ -0,0 +1,309 @@ +=========== +JDBC Guide +=========== + +.. important:: + The JDBC interface is experimental and not production-ready at this stage. APIs and behaviors are subject to change. + +This guide covers using JDBC to interact with the FoundationDB Record Layer's SQL interface. + +Driver Setup +============ + +The Record Layer provides two JDBC drivers depending on your deployment architecture: + +- **Embedded Driver**: For applications running in the same JVM as the Record Layer +- **Server Driver**: For client applications connecting to a remote Record Layer server via gRPC + +.. tab-set:: + + .. tab-item:: Embedded Driver + :sync: embedded + + The embedded driver class ``com.apple.foundationdb.relational.api.EmbeddedRelationalDriver`` is registered with Java's `Service Loader `_ and automatically discovered by the JDBC ``DriverManager`` when on the classpath. + + **Maven Dependency:** + + .. code-block:: xml + + + org.foundationdb + fdb-relational-core + VERSION_NUMBER + + + Replace ``VERSION_NUMBER`` with the appropriate version for your project. + + .. tab-item:: Server Driver + :sync: server + + The server driver class ``com.apple.foundationdb.relational.jdbc.JDBCRelationalDriver`` connects to a remote Record Layer server via gRPC. It is also registered with Java's `Service Loader `_. + + **Maven Dependency:** + + .. code-block:: xml + + + org.foundationdb + fdb-relational-jdbc + VERSION_NUMBER + + + Replace ``VERSION_NUMBER`` with the appropriate version for your project. + +Connection Strings +================== + +The connection string format differs between the embedded and server drivers: + +.. tab-set:: + + .. tab-item:: Embedded Driver + :sync: embedded + + JDBC URLs use the ``jdbc:embed:`` prefix followed by the database path and query parameters: + + :: + + jdbc:embed:/?schema= + + **Examples:** + + Connecting to the system database ``__SYS`` using the ``CATALOG`` schema: + + .. code-block:: java + + String url = "jdbc:embed:/__SYS?schema=CATALOG"; + Connection connection = DriverManager.getConnection(url); + + Connecting to a custom database: + + .. code-block:: java + + String url = "jdbc:embed:/FRL/YCSB?schema=YCSB"; + Connection connection = DriverManager.getConnection(url); + + **Connection Parameters:** + + - **Database path**: The path to the database (e.g., ``/__SYS``, ``/FRL/YCSB``) + - **schema**: (Required) The schema to use for the connection + + .. tab-item:: Server Driver + :sync: server + + JDBC URLs use the ``jdbc:relational://`` prefix followed by the server host, database path, and query parameters: + + :: + + jdbc:relational://:/?schema= + + **Examples:** + + Connecting to a Record Layer server: + + .. code-block:: java + + String url = "jdbc:relational://localhost:7243/__SYS?schema=CATALOG"; + Connection connection = DriverManager.getConnection(url); + + Connecting to a custom database on a remote server: + + .. code-block:: java + + String url = "jdbc:relational://server.example.com:7243/FRL/YCSB?schema=YCSB"; + Connection connection = DriverManager.getConnection(url); + + **Connection Parameters:** + + - **host**: The hostname or IP address of the Record Layer server + - **port**: The port number (default: 7243) + - **Database path**: The path to the database (e.g., ``/__SYS``, ``/FRL/YCSB``) + - **schema**: (Required) The schema to use for the connection + +Basic Usage +=========== + +Once you have a connection, all JDBC operations work the same way regardless of which driver you're using. + +Creating a Connection +--------------------- + +.. tab-set:: + + .. tab-item:: Embedded Driver + :sync: embedded + + .. code-block:: java + + import java.sql.Connection; + import java.sql.DriverManager; + import java.sql.SQLException; + + public class JDBCExample { + public static void main(String[] args) { + String url = "jdbc:embed:/mydb?schema=myschema"; + + try (Connection conn = DriverManager.getConnection(url)) { + // Use the connection + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + + .. tab-item:: Server Driver + :sync: server + + .. code-block:: java + + import java.sql.Connection; + import java.sql.DriverManager; + import java.sql.SQLException; + + public class JDBCExample { + public static void main(String[] args) { + String url = "jdbc:relational://localhost:7243/mydb?schema=myschema"; + + try (Connection conn = DriverManager.getConnection(url)) { + // Use the connection + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + +Executing Queries +----------------- + +Use ``Statement`` for simple queries: + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/BasicSnippets.java + :language: java + :start-after: // tag::simple-query[] + :end-before: // end::simple-query[] + :dedent: 8 + +Executing Updates +----------------- + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/BasicSnippets.java + :language: java + :start-after: // tag::simple-update[] + :end-before: // end::simple-update[] + :dedent: 8 + +Prepared Statements +=================== + +Prepared statements provide better security by using parameter binding to prevent SQL injection: + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/BasicSnippets.java + :language: java + :start-after: // tag::prepared-insert[] + :end-before: // end::prepared-insert[] + :dedent: 8 + +Querying with Prepared Statements +---------------------------------- + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/BasicSnippets.java + :language: java + :start-after: // tag::prepared-query[] + :end-before: // end::prepared-query[] + :dedent: 8 + +Transaction Management +====================== + +Auto-Commit Mode +---------------- + +By default, JDBC connections operate in auto-commit mode where each statement is automatically committed. You can check and modify this behavior: + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/BasicSnippets.java + :language: java + :start-after: // tag::autocommit[] + :end-before: // end::autocommit[] + :dedent: 8 + +Manual Transaction Control +--------------------------- + +For operations that need to execute as a unit, disable auto-commit and manually control transactions: + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/BasicSnippets.java + :language: java + :start-after: // tag::transaction[] + :end-before: // end::transaction[] + :dedent: 8 + +Working with ResultSets +======================= + +Retrieving Data +--------------- + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/BasicSnippets.java + :language: java + :start-after: // tag::resultset-basic[] + :end-before: // end::resultset-basic[] + :dedent: 8 + +Handling Null Values +-------------------- + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/BasicSnippets.java + :language: java + :start-after: // tag::null-handling[] + :end-before: // end::null-handling[] + :dedent: 8 + +ResultSet Metadata +------------------ + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/BasicSnippets.java + :language: java + :start-after: // tag::metadata[] + :end-before: // end::metadata[] + :dedent: 8 + +Error Handling +============== + +Proper error handling is essential for robust JDBC applications: + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/BasicSnippets.java + :language: java + :start-after: // tag::error-handling[] + :end-before: // end::error-handling[] + :dedent: 8 + +Database Metadata +================= + +Retrieve information about the database structure: + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/BasicSnippets.java + :language: java + :start-after: // tag::database-metadata[] + :end-before: // end::database-metadata[] + :dedent: 8 + +Complete Example +================ + +Here's a complete example demonstrating common JDBC operations: + +.. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/ProductManagerEmbedded.java + :language: java + :start-after: // tag::complete[] + :end-before: // end::complete[] + :dedent: 0 + +See Also +======== + +- :doc:`advanced` - Working with STRUCTs and ARRAYs +- :doc:`../SQL_Reference` - Complete SQL syntax reference +- :doc:`../GettingStarted` - Introduction to the Record Layer +- :doc:`../reference/Databases_Schemas_SchemaTemplates` - Understanding the data model diff --git a/docs/sphinx/source/jdbc/direct_access.rst b/docs/sphinx/source/jdbc/direct_access.rst new file mode 100644 index 0000000000..10ad4396dd --- /dev/null +++ b/docs/sphinx/source/jdbc/direct_access.rst @@ -0,0 +1,434 @@ +====================== +Key-Based Data Access +====================== + +.. important:: + **Transition API**: The Key-Based Data Access API is provided primarily to ease migration from key-value based operations to SQL-based queries. While it remains supported, it is **not the recommended approach for new applications**. For most use cases, SQL queries provide better flexibility, optimization, and maintainability. This API may evolve or be deprecated in future versions as the SQL interface matures. + +The Key-Based Data Access API provides programmatic methods for common database operations without writing SQL. These operations work directly with primary keys and support: + +- **Scan**: Range queries over primary key prefixes +- **Get**: Single record lookup by complete primary key +- **Insert**: Programmatic record insertion with ``RelationalStruct`` +- **Delete**: Key-based deletion (single, batch, and range) + +For more complex queries, filtering, joins, or aggregations, use the :doc:`SQL query language <../SQL_Reference>` instead. + +Getting Started +=============== + +The Key-Based Data Access methods are available on ``RelationalStatement``, which extends the standard JDBC ``Statement``. To access these methods, unwrap your statement: + +.. tab-set:: + + .. tab-item:: Embedded Driver + :sync: embedded + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippets.java + :language: java + :start-after: // tag::unwrap-statement[] + :end-before: // end::unwrap-statement[] + :dedent: 8 + + .. tab-item:: Server Driver + :sync: server + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippetsServer.java + :language: java + :start-after: // tag::unwrap-statement[] + :end-before: // end::unwrap-statement[] + :dedent: 8 + +Scan Operations +=============== + +Scans retrieve multiple records that match a primary key prefix. The API supports pagination through continuations for handling large result sets. + +Basic Scan +---------- + +To scan all records in a table, use an empty ``KeySet``: + +.. tab-set:: + + .. tab-item:: Embedded Driver + :sync: embedded + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippets.java + :language: java + :start-after: // tag::scan-basic[] + :end-before: // end::scan-basic[] + :dedent: 8 + + .. tab-item:: Server Driver + :sync: server + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippetsServer.java + :language: java + :start-after: // tag::scan-basic[] + :end-before: // end::scan-basic[] + :dedent: 8 + +Scan with Key Prefix +--------------------- + +Narrow the scan to records matching a primary key prefix. The key columns must form a **contiguous prefix** of the primary key: + +.. tab-set:: + + .. tab-item:: Embedded Driver + :sync: embedded + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippets.java + :language: java + :start-after: // tag::scan-prefix[] + :end-before: // end::scan-prefix[] + :dedent: 8 + + .. tab-item:: Server Driver + :sync: server + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippetsServer.java + :language: java + :start-after: // tag::scan-prefix[] + :end-before: // end::scan-prefix[] + :dedent: 8 + +**Key Prefix Rules:** + +Given a table with ``PRIMARY KEY(a, b, c)``: + +- ✓ Valid: ``{a: 1}``, ``{a: 1, b: 2}``, ``{a: 1, b: 2, c: 3}`` +- ✗ Invalid: ``{b: 2}`` (not a prefix), ``{a: 1, c: 3}`` (skips ``b``) + +Scan with Continuation +----------------------- + +For large result sets, use continuations to paginate through results: + +.. tab-set:: + + .. tab-item:: Embedded Driver + :sync: embedded + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippets.java + :language: java + :start-after: // tag::scan-continuation[] + :end-before: // end::scan-continuation[] + :dedent: 8 + + .. tab-item:: Server Driver + :sync: server + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippetsServer.java + :language: java + :start-after: // tag::scan-continuation[] + :end-before: // end::scan-continuation[] + :dedent: 8 + +The continuation points to the **first unread row** after the current position in the ``ResultSet``. + +Get Operations +============== + +Get operations retrieve a single record by its complete primary key. The result set will contain either 0 or 1 row. + +Basic Get +--------- + +.. tab-set:: + + .. tab-item:: Embedded Driver + :sync: embedded + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippets.java + :language: java + :start-after: // tag::get-basic[] + :end-before: // end::get-basic[] + :dedent: 8 + + .. tab-item:: Server Driver + :sync: server + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippetsServer.java + :language: java + :start-after: // tag::get-basic[] + :end-before: // end::get-basic[] + :dedent: 8 + +Get with Composite Key +---------------------- + +For tables with composite primary keys, provide all key columns: + +.. tab-set:: + + .. tab-item:: Embedded Driver + :sync: embedded + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippets.java + :language: java + :start-after: // tag::get-composite[] + :end-before: // end::get-composite[] + :dedent: 8 + + .. tab-item:: Server Driver + :sync: server + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippetsServer.java + :language: java + :start-after: // tag::get-composite[] + :end-before: // end::get-composite[] + :dedent: 8 + +**Important**: Unlike scans, Get requires a **complete primary key**. Incomplete keys will cause an error. + +Insert Operations +================= + +Insert operations add records to a table using ``RelationalStruct`` objects. The struct must contain values for all required columns. + +Single Insert +------------- + +.. tab-set:: + + .. tab-item:: Embedded Driver + :sync: embedded + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippets.java + :language: java + :start-after: // tag::insert-single[] + :end-before: // end::insert-single[] + :dedent: 8 + + .. tab-item:: Server Driver + :sync: server + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippetsServer.java + :language: java + :start-after: // tag::insert-single[] + :end-before: // end::insert-single[] + :dedent: 8 + +Batch Insert +------------ + +Insert multiple records in a single operation for better performance: + +.. tab-set:: + + .. tab-item:: Embedded Driver + :sync: embedded + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippets.java + :language: java + :start-after: // tag::insert-batch[] + :end-before: // end::insert-batch[] + :dedent: 8 + + .. tab-item:: Server Driver + :sync: server + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippetsServer.java + :language: java + :start-after: // tag::insert-batch[] + :end-before: // end::insert-batch[] + :dedent: 8 + +Insert with Replace on Duplicate +--------------------------------- + +Use the ``REPLACE_ON_DUPLICATE_PK`` option to update existing records instead of failing on primary key conflicts: + +.. tab-set:: + + .. tab-item:: Embedded Driver + :sync: embedded + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippets.java + :language: java + :start-after: // tag::insert-replace[] + :end-before: // end::insert-replace[] + :dedent: 8 + + .. tab-item:: Server Driver + :sync: server + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippetsServer.java + :language: java + :start-after: // tag::insert-replace[] + :end-before: // end::insert-replace[] + :dedent: 8 + +Delete Operations +================= + +Delete operations remove records by their primary keys. Three variants are supported: single delete, batch delete, and range delete. + +Single Delete +------------- + +.. tab-set:: + + .. tab-item:: Embedded Driver + :sync: embedded + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippets.java + :language: java + :start-after: // tag::delete-single[] + :end-before: // end::delete-single[] + :dedent: 8 + + .. tab-item:: Server Driver + :sync: server + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippetsServer.java + :language: java + :start-after: // tag::delete-single[] + :end-before: // end::delete-single[] + :dedent: 8 + +Batch Delete +------------ + +Delete multiple records by providing a collection of keys: + +.. tab-set:: + + .. tab-item:: Embedded Driver + :sync: embedded + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippets.java + :language: java + :start-after: // tag::delete-batch[] + :end-before: // end::delete-batch[] + :dedent: 8 + + .. tab-item:: Server Driver + :sync: server + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippetsServer.java + :language: java + :start-after: // tag::delete-batch[] + :end-before: // end::delete-batch[] + :dedent: 8 + +Range Delete +------------ + +Delete all records matching a primary key prefix using ``executeDeleteRange``: + +.. tab-set:: + + .. tab-item:: Embedded Driver + :sync: embedded + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippets.java + :language: java + :start-after: // tag::delete-range[] + :end-before: // end::delete-range[] + :dedent: 8 + + .. tab-item:: Server Driver + :sync: server + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippetsServer.java + :language: java + :start-after: // tag::delete-range[] + :end-before: // end::delete-range[] + :dedent: 8 + +**Warning**: Range deletes can affect many records. Use with caution. + +Working with KeySet +=================== + +``KeySet`` specifies primary key values for direct access operations. It supports fluent builder-style construction: + +.. code-block:: java + + KeySet key = new KeySet() + .setKeyColumn("column1", value1) + .setKeyColumn("column2", value2); + +**Key Points:** + +- Column names are case-sensitive and should match your schema +- For scans and range deletes, provide a contiguous key prefix +- For gets and point deletes, provide the complete primary key +- Use ``KeySet.EMPTY`` or ``new KeySet()`` to match all records (full table scan) + +Options +======= + +The ``Options`` class configures execution behavior for direct access operations. Common options include: + +CONTINUATION +------------ + +Specifies where to resume a scan operation (see continuation example above). + +INDEX_HINT +---------- + +Suggests a specific index for FRL to use: + +.. tab-set:: + + .. tab-item:: Embedded Driver + :sync: embedded + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippets.java + :language: java + :start-after: // tag::index-hint[] + :end-before: // end::index-hint[] + :dedent: 8 + + .. tab-item:: Server Driver + :sync: server + + .. literalinclude:: ../../../../examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippetsServer.java + :language: java + :start-after: // tag::index-hint[] + :end-before: // end::index-hint[] + :dedent: 8 + +MAX_ROWS +-------- + +Limits the number of rows returned in a single scan before prompting for continuation: + +.. code-block:: java + + Options options = Options.builder() + .withOption(Options.Name.MAX_ROWS, 100) + .build(); + +REPLACE_ON_DUPLICATE_PK +----------------------- + +When inserting, replaces existing records with conflicting primary keys instead of failing (see insert example above). + +Building Options +---------------- + +Use the builder pattern to construct options: + +.. code-block:: java + + Options options = Options.builder() + .withOption(Options.Name.INDEX_HINT, "my_index") + .withOption(Options.Name.MAX_ROWS, 50) + .build(); + + // Or use Options.NONE for default behavior + relStmt.executeScan("table", keySet, Options.NONE); + +See Also +======== + +- :doc:`basic` - Basic JDBC usage patterns +- :doc:`advanced` - Working with STRUCTs and ARRAYs +- :doc:`../SQL_Reference` - Complete SQL syntax reference +- :doc:`../reference/Databases_Schemas_SchemaTemplates` - Understanding the data model diff --git a/docs/sphinx/source/jdbc/index.rst b/docs/sphinx/source/jdbc/index.rst new file mode 100644 index 0000000000..c4178e004c --- /dev/null +++ b/docs/sphinx/source/jdbc/index.rst @@ -0,0 +1,15 @@ +=========== +JDBC Guide +=========== + +.. important:: + The JDBC interface is experimental and not production-ready at this stage. APIs and behaviors are subject to change. + +The FoundationDB Record Layer provides JDBC drivers for accessing your data using standard Java Database Connectivity APIs. These guides cover both basic JDBC operations and advanced features specific to the Record Layer. + +.. toctree:: + :maxdepth: 2 + + Basic Usage + Advanced Features + Key-Based Data Access diff --git a/docs/sphinx/source/reference/Direct_Access_Api.rst b/docs/sphinx/source/reference/Direct_Access_Api.rst deleted file mode 100644 index 572a9bcaa4..0000000000 --- a/docs/sphinx/source/reference/Direct_Access_Api.rst +++ /dev/null @@ -1,108 +0,0 @@ -================= -Direct Access API -================= - -The Relational Layer's Direct Access API is a simplified API for accessing data within specific ranges. -It avoids using SQL directly, but it uses the same interface as the SQL connection to allow intermingling of -SQL queries with these simpler operations. The intent is to provide highly simplified access for common -current use-cases (e.g., scanning records within a specific range), but it is *highly* limited. For more -powerful query execution, use the :doc:`SQL query language ` instead. - -The Direct Access API consists of the following operations: - -* Scan -* Get -* Insert -* Delete - -Scan -#### - -Scans can access all records which fit within a specified primary key or index key range. These scans can be arbitrarily large, based on the data and the range of interest, and thus continuations are provided to enable paginated queries. For example - -.. code-block:: java - - try (RelationalStatement statement = createStatement()) { - TableScan scan = TableScan.newBuilder() - .setStartKey(, ) - // fill in the rest of the starting keys... - .setEndKey(, ) - // fill in the rest of the ending keys... - .build(); - Continuation continuation = Continuation.BEGIN; - int continuationBatchSize = 10; - while (!continuation.isEnd()) { - int rowCount = 0; - try (RelationalResultSet scanResultSet = statement.executeScan(, scan, ) { - while(scanResultSet.next()) { - //process a returned record - rowCount++; - if (rowCount == continuationBatchSize) { - continuation = scanResultSet.getContinuation(); - break; - } - } - } - } - } - - -Scan keys can be partial, as long as they are contiguous. You can specify :java:`pk1, pk2`, but not :java:`pk1, pk3`; -otherwise, the API will throw an error. - -The returned :java:`ResultSet` can access a continuation, which is a pointer to the *first row which has not yet been read*. -Put another way, if the *current* location in the :java:`ResultSet` is position :java:`X`, then the continuation will point to position :java:`X+1`. -This can be used as a marker for restarting a given query. By passing the continuation to the API when executing the -same statement again, the continuation will be used to resume the execution at the specified location. - -Get -### - -The Get operation provides the ability to specify the primary key of a certain record, and quickly return the record -associated with that Primary Key from a specific table. If the record does not exist, then the result set will be empty. - -.. code-block:: java - - try (RelationalStatement statement = createStatement()) { - KeySet primaryKey = new KeySet() - .setKeyColumn("", ) - // set the other values for the primary key of the table... - try (RelationalResultSet recordResultSet = statement.executeGet(, primaryKey, )) { - if (recordResultSet.next()){ - //process the returned record -- there will always be no more than 1 record in the returned result set. - } - } - } - -If you specify an incomplete :java:`KeySet` (i.e., an incomplete primary key), then the API will throw an error. - -Insert -###### - -The Insert API provides a way to insert a data element into a specific table using programmatic API. The API requires -building a :java:`DynamicMessage`, as follows: - -.. code-block:: java - - try (RelationalStatement statement = createStatement()) { - DynamicMessageBuilder messageBuilder = statement.getMessageBuilder() - .setField(, ) - // set the other fields in the record, including nested or repeated structures... - statement.executeInsert(, messageBuilder.build(), ); - } - -You can also insert multiple records together in batch, using an iterable interface of build records. - -Delete -###### - -Deletes are very similar to inserts, except that you specify the primary keys of the rows that you want to delete: - -.. code-block:: java - - try (RelationalStatement statement = createStatement()) { - KeySet primaryKey = new KeySet() - .setKeyColumn("", ) - // set the other values for the primary key of the table... - statement.executeDelete(, primaryKey, ); - } diff --git a/docs/sphinx/source/reference/sql_commands/DQL.rst b/docs/sphinx/source/reference/sql_commands/DQL.rst index 73afe4a951..eb7cc83453 100644 --- a/docs/sphinx/source/reference/sql_commands/DQL.rst +++ b/docs/sphinx/source/reference/sql_commands/DQL.rst @@ -3,7 +3,7 @@ DQL === The Relational Layer supports a full query engine, complete with query planning and index selection. We generally recommend -using the query planner whenever possible over using the :doc:`../Direct_Access_Api` as it is more powerful and more capable, and +using the query planner whenever possible over using the :doc:`../../jdbc/direct_access` as it is more powerful and more capable, and it is designed to make intelligent choices regarding query planning decisions like appropriate index selection. The language is heavily inspired by SQL. However, the Relational Layer *does* make some important changes to SQL to accommodate diff --git a/examples/examples.gradle b/examples/examples.gradle index 91e4310e93..fc7c0eead5 100644 --- a/examples/examples.gradle +++ b/examples/examples.gradle @@ -24,6 +24,9 @@ apply plugin: 'application' def coreProject = ":${ext.coreProjectName}" dependencies { implementation project(coreProject) + implementation project(':fdb-relational-api') + implementation project(':fdb-relational-core') + implementation project(':fdb-relational-jdbc') implementation(libs.protobuf) implementation(libs.slf4j.api) runtimeOnly(libs.log4j.slf4jBinding) // binding diff --git a/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/AdvancedSnippets.java b/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/AdvancedSnippets.java new file mode 100644 index 0000000000..6ddfb9b02e --- /dev/null +++ b/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/AdvancedSnippets.java @@ -0,0 +1,370 @@ +/* + * AdvancedSnippets.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2025 Apple Inc. and the FoundationDB project 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.apple.foundationdb.relational.jdbc.examples; + +import com.apple.foundationdb.relational.api.EmbeddedRelationalArray; +import com.apple.foundationdb.relational.api.EmbeddedRelationalStruct; +import com.apple.foundationdb.relational.api.RelationalPreparedStatement; +import com.apple.foundationdb.relational.api.RelationalResultSet; +import com.apple.foundationdb.relational.api.RelationalStruct; + +import java.sql.*; + +/** + * Code snippets for JDBC Guide advanced documentation. + * This class is not meant to be run, but contains tagged sections referenced by the documentation. + */ +public class AdvancedSnippets { + private static final String url = "jdbc:embed:/FRL/shop?schema=SHOP"; + + public static void readStruct() throws SQLException { + // tag::read-struct[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT reviewer FROM reviewers WHERE id = 1")) { + // Unwrap to RelationalResultSet to access getStruct() + RelationalResultSet relationalRs = rs.unwrap(RelationalResultSet.class); + + if (relationalRs.next()) { + RelationalStruct reviewer = relationalRs.getStruct("reviewer"); + + // Access STRUCT fields by name + long id = reviewer.getLong("ID"); + String name = reviewer.getString("NAME"); + String email = reviewer.getString("EMAIL"); + + System.out.printf("ID: %d, Name: %s, Email: %s%n", id, name, email); + } + } + } + } + // end::read-struct[] + } + + public static void createStructSimple() throws SQLException { + // tag::create-struct-simple[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + String sql = "INSERT INTO reviewers (id, reviewer) VALUES (?, ?)"; + try (RelationalPreparedStatement pstmt = + conn.prepareStatement(sql).unwrap(RelationalPreparedStatement.class)) { + + // Build a STRUCT to insert into the reviewer column + RelationalStruct reviewer = EmbeddedRelationalStruct.newBuilder() + .addLong("ID", 1L) + .addString("NAME", "Anthony Bourdain") + .addString("EMAIL", "abourdain@example.com") + .build(); + + pstmt.setLong(1, 1L); + pstmt.setObject(2, reviewer); + + int inserted = pstmt.executeUpdate(); + System.out.println("Rows inserted: " + inserted); + + conn.commit(); + } + } + // end::create-struct-simple[] + } + + public static void createStructWithNull() throws SQLException { + // tag::create-struct-null[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + String sql = "INSERT INTO reviewers (id, reviewer) VALUES (?, ?)"; + try (RelationalPreparedStatement pstmt = + conn.prepareStatement(sql).unwrap(RelationalPreparedStatement.class)) { + + // Create STRUCT with NULL email field + RelationalStruct reviewer = EmbeddedRelationalStruct.newBuilder() + .addLong("ID", 1L) + .addString("NAME", "Anthony Bourdain") + .addObject("EMAIL", null) // Explicitly set NULL + .build(); + + pstmt.setLong(1, 1L); + pstmt.setObject(2, reviewer); + + int inserted = pstmt.executeUpdate(); + conn.commit(); + } + } + // end::create-struct-null[] + } + + public static void readArrayBasic() throws SQLException { + // tag::read-array-basic[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT tags FROM products WHERE id = 1")) { + if (rs.next()) { + Array tagsArray = rs.getArray("tags"); + + if (tagsArray != null) { + // Method 1: Get array as Object[] + Object[] tags = (Object[]) tagsArray.getArray(); + for (Object tag : tags) { + System.out.println("Tag: " + tag); + } + } + } + } + } + } + // end::read-array-basic[] + } + + public static void readArrayResultSet() throws SQLException { + // tag::read-array-resultset[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT tags FROM products WHERE id = 1")) { + if (rs.next()) { + Array tagsArray = rs.getArray("tags"); + + if (tagsArray != null) { + // Method 2: Get array as ResultSet + try (ResultSet arrayRs = tagsArray.getResultSet()) { + // ResultSet has two columns: + // Column 1: index (1-based) + // Column 2: value + while (arrayRs.next()) { + int index = arrayRs.getInt(1); + String value = arrayRs.getString(2); + System.out.printf("tags[%d] = %s%n", index, value); + } + } + } + } + } + } + } + // end::read-array-resultset[] + } + + public static void createArrayBasic() throws SQLException { + // tag::create-array-basic[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + String sql = "INSERT INTO products (id, name, tags) VALUES (?, ?, ?)"; + try (RelationalPreparedStatement pstmt = + conn.prepareStatement(sql).unwrap(RelationalPreparedStatement.class)) { + + pstmt.setLong(1, 1L); + pstmt.setString(2, "Widget"); + pstmt.setArray(3, EmbeddedRelationalArray.newBuilder() + .addAll("electronics", "gadget", "popular") + .build()); + + int rowsAffected = pstmt.executeUpdate(); + System.out.println("Rows inserted: " + rowsAffected); + + conn.commit(); + } + } + // end::create-array-basic[] + } + + public static void createArrayTypes() throws SQLException { + // tag::create-array-types[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + String sql = "INSERT INTO data_table (id, integers, floats, booleans) " + + "VALUES (?, ?, ?, ?)"; + try (RelationalPreparedStatement pstmt = + conn.prepareStatement(sql).unwrap(RelationalPreparedStatement.class)) { + + pstmt.setLong(1, 1L); + + // Array of integers + pstmt.setArray(2, EmbeddedRelationalArray.newBuilder() + .addAll(10, 20, 30, 40) + .build()); + + // Array of floats + pstmt.setArray(3, EmbeddedRelationalArray.newBuilder() + .addAll(1.5f, 2.5f, 3.5f) + .build()); + + // Array of booleans + pstmt.setArray(4, EmbeddedRelationalArray.newBuilder() + .addAll(true, false, true) + .build()); + + pstmt.executeUpdate(); + conn.commit(); + } + } + // end::create-array-types[] + } + + public static void arrayNull() throws SQLException { + // tag::array-null[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + String sql = "INSERT INTO products (id, name, tags) VALUES (?, ?, ?)"; + try (RelationalPreparedStatement pstmt = + conn.prepareStatement(sql).unwrap(RelationalPreparedStatement.class)) { + + pstmt.setLong(1, 1L); + pstmt.setString(2, "Widget"); + pstmt.setNull(3, Types.ARRAY); // Set NULL array + + pstmt.executeUpdate(); + conn.commit(); + } + } + // end::array-null[] + } + + public static void arrayMetadata() throws SQLException { + // tag::array-metadata[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT tags FROM products WHERE id = 1")) { + if (rs.next()) { + Array tagsArray = rs.getArray("tags"); + + if (tagsArray != null) { + // Get array metadata + int baseType = tagsArray.getBaseType(); + String baseTypeName = tagsArray.getBaseTypeName(); + + System.out.println("Array base type: " + baseType); + System.out.println("Array base type name: " + baseTypeName); + + // Get array elements + Object[] elements = (Object[]) tagsArray.getArray(); + System.out.println("Array length: " + elements.length); + } + } + } + } + } + // end::array-metadata[] + } + + public static void structMetadata() throws SQLException { + // tag::struct-metadata[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT reviewer FROM reviewers WHERE id = 1")) { + // Unwrap to RelationalResultSet to access getStruct() + RelationalResultSet relationalRs = rs.unwrap(RelationalResultSet.class); + + if (relationalRs.next()) { + RelationalStruct reviewer = relationalRs.getStruct("reviewer"); + + if (reviewer != null) { + // Get STRUCT metadata + var metaData = reviewer.getMetaData(); + int columnCount = metaData.getColumnCount(); + + System.out.println("STRUCT has " + columnCount + " fields:"); + + // Iterate through all fields + for (int i = 1; i <= columnCount; i++) { + String columnName = metaData.getColumnName(i); + String columnTypeName = metaData.getColumnTypeName(i); + int columnType = metaData.getColumnType(i); + + System.out.printf(" Field %d: %s (%s, type=%d)%n", + i, columnName, columnTypeName, columnType); + } + } + } + } + } + } + // end::struct-metadata[] + } + + public static void arrayOfStructs() throws SQLException { + // tag::array-of-structs[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + String sql = "INSERT INTO orders (id, items) VALUES (?, ?)"; + try (RelationalPreparedStatement pstmt = + conn.prepareStatement(sql).unwrap(RelationalPreparedStatement.class)) { + + pstmt.setLong(1, 1L); + + // Create an array of STRUCT values + pstmt.setArray(2, EmbeddedRelationalArray.newBuilder() + .addAll( + EmbeddedRelationalStruct.newBuilder() + .addInt("product_id", 11) + .addString("product_name", "Widget A") + .addInt("quantity", 2) + .build(), + EmbeddedRelationalStruct.newBuilder() + .addInt("product_id", 22) + .addString("product_name", "Widget B") + .addInt("quantity", 5) + .build() + ) + .build()); + + pstmt.executeUpdate(); + conn.commit(); + } + } + // end::array-of-structs[] + } + + public static void structContainingArrays() throws SQLException { + // tag::struct-containing-arrays[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + String sql = "INSERT INTO products (id, product_info) VALUES (?, ?)"; + try (RelationalPreparedStatement pstmt = + conn.prepareStatement(sql).unwrap(RelationalPreparedStatement.class)) { + + // Create STRUCT with embedded ARRAY fields + RelationalStruct product = EmbeddedRelationalStruct.newBuilder() + .addLong("ID", 1L) + .addString("NAME", "Multi-tool") + .addArray("TAGS", EmbeddedRelationalArray.newBuilder() + .addAll("versatile", "portable", "durable") + .build()) + .addArray("RATINGS", EmbeddedRelationalArray.newBuilder() + .addAll(4.5, 4.8, 4.2, 4.9) + .build()) + .build(); + + pstmt.setLong(1, 1L); + pstmt.setObject(2, product); + + int inserted = pstmt.executeUpdate(); + conn.commit(); + } + } + // end::struct-containing-arrays[] + } +} diff --git a/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/AdvancedSnippetsServer.java b/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/AdvancedSnippetsServer.java new file mode 100644 index 0000000000..d45c63d324 --- /dev/null +++ b/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/AdvancedSnippetsServer.java @@ -0,0 +1,101 @@ +/* + * AdvancedSnippetsServer.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2025 Apple Inc. and the FoundationDB project 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.apple.foundationdb.relational.jdbc.examples; + +import com.apple.foundationdb.relational.api.RelationalPreparedStatement; +import com.apple.foundationdb.relational.api.RelationalStruct; +import com.apple.foundationdb.relational.jdbc.JDBCRelationalArray; +import com.apple.foundationdb.relational.jdbc.JDBCRelationalStruct; + +import java.sql.*; + +/** + * Code snippets for JDBC Guide advanced documentation using the server driver. + * This class is not meant to be run, but contains tagged sections referenced by the documentation. + */ +public class AdvancedSnippetsServer { + private static final String url = "jdbc:relational://localhost:7243/FRL/shop?schema=SHOP"; + + public static void arrayOfStructs() throws SQLException { + // tag::array-of-structs[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + String sql = "INSERT INTO orders (id, items) VALUES (?, ?)"; + try (RelationalPreparedStatement pstmt = + conn.prepareStatement(sql).unwrap(RelationalPreparedStatement.class)) { + + pstmt.setLong(1, 1L); + + // Create an array of STRUCT values + pstmt.setArray(2, JDBCRelationalArray.newBuilder() + .addAll( + JDBCRelationalStruct.newBuilder() + .addInt("product_id", 11) + .addString("product_name", "Widget A") + .addInt("quantity", 2) + .build(), + JDBCRelationalStruct.newBuilder() + .addInt("product_id", 22) + .addString("product_name", "Widget B") + .addInt("quantity", 5) + .build() + ) + .build()); + + pstmt.executeUpdate(); + conn.commit(); + } + } + // end::array-of-structs[] + } + + public static void structContainingArrays() throws SQLException { + // tag::struct-containing-arrays[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + String sql = "INSERT INTO products (id, product_info) VALUES (?, ?)"; + try (RelationalPreparedStatement pstmt = + conn.prepareStatement(sql).unwrap(RelationalPreparedStatement.class)) { + + // Create STRUCT with embedded ARRAY fields + RelationalStruct product = JDBCRelationalStruct.newBuilder() + .addLong("ID", 1L) + .addString("NAME", "Multi-tool") + .addArray("TAGS", JDBCRelationalArray.newBuilder() + .addAll("versatile", "portable", "durable") + .build()) + .addArray("RATINGS", JDBCRelationalArray.newBuilder() + .addAll(4.5, 4.8, 4.2, 4.9) + .build()) + .build(); + + pstmt.setLong(1, 1L); + pstmt.setObject(2, product); + + int inserted = pstmt.executeUpdate(); + conn.commit(); + } + } + // end::struct-containing-arrays[] + } +} diff --git a/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/BasicSnippets.java b/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/BasicSnippets.java new file mode 100644 index 0000000000..0af24ea0bf --- /dev/null +++ b/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/BasicSnippets.java @@ -0,0 +1,225 @@ +/* + * BasicSnippets.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2025 Apple Inc. and the FoundationDB project 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.apple.foundationdb.relational.jdbc.examples; + +import java.sql.*; + +/** + * Code snippets for JDBC Guide basic documentation. + * This class is not meant to be run, but contains tagged sections referenced by the documentation. + */ +public class BasicSnippets { + private static final String url = "jdbc:embed:/FRL/shop?schema=SHOP"; + + public static void simpleQuery() throws SQLException { + // tag::simple-query[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM products WHERE price > 100")) { + while (rs.next()) { + long id = rs.getLong("id"); + String name = rs.getString("name"); + long price = rs.getLong("price"); + System.out.println(id + ": " + name + " - $" + price); + } + } + } + } + // end::simple-query[] + } + + public static void simpleUpdate() throws SQLException { + // tag::simple-update[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + int rowsAffected = stmt.executeUpdate( + "INSERT INTO products (id, name, price) VALUES (1, 'Widget', 100)" + ); + System.out.println("Rows affected: " + rowsAffected); + } + } + // end::simple-update[] + } + + public static void preparedInsert() throws SQLException { + // tag::prepared-insert[] + String sql = "INSERT INTO products (id, name, price, stock) VALUES (?, ?, ?, ?)"; + + try (Connection conn = DriverManager.getConnection(url)) { + try (PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setLong(1, 1); + pstmt.setString(2, "Widget A"); + pstmt.setLong(3, 100); + pstmt.setInt(4, 50); + + int rowsAffected = pstmt.executeUpdate(); + System.out.println("Rows inserted: " + rowsAffected); + } + } + // end::prepared-insert[] + } + + public static void preparedQuery() throws SQLException { + // tag::prepared-query[] + String sql = "SELECT * FROM products WHERE category = ? AND price >= ?"; + + try (Connection conn = DriverManager.getConnection(url)) { + try (PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setString(1, "Electronics"); + pstmt.setLong(2, 100); + + try (ResultSet rs = pstmt.executeQuery()) { + while (rs.next()) { + System.out.println(rs.getString("name") + ": $" + rs.getLong("price")); + } + } + } + } + // end::prepared-query[] + } + + public static void autocommit() throws SQLException { + // tag::autocommit[] + try (Connection conn = DriverManager.getConnection(url)) { + System.out.println("Auto-commit: " + conn.getAutoCommit()); + + // Disable auto-commit for manual transaction control + conn.setAutoCommit(false); + } + // end::autocommit[] + } + + public static void transaction() throws SQLException { + // tag::transaction[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("INSERT INTO accounts (id, balance) VALUES (1, 1000)"); + stmt.executeUpdate("INSERT INTO accounts (id, balance) VALUES (2, 500)"); + stmt.executeUpdate("UPDATE accounts SET balance = balance - 100 WHERE id = 1"); + stmt.executeUpdate("UPDATE accounts SET balance = balance + 100 WHERE id = 2"); + + conn.commit(); + System.out.println("Transaction committed successfully"); + } catch (SQLException e) { + conn.rollback(); + System.out.println("Transaction rolled back due to error: " + e.getMessage()); + throw e; + } + } + // end::transaction[] + } + + public static void resultsetBasic() throws SQLException { + // tag::resultset-basic[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM products ORDER BY price")) { + while (rs.next()) { + long id = rs.getLong("id"); + String name = rs.getString("name"); + long price = rs.getLong("price"); + + System.out.printf("ID: %d, Name: %s, Price: %d%n", id, name, price); + } + } + } + } + // end::resultset-basic[] + } + + public static void nullHandling() throws SQLException { + // tag::null-handling[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM products")) { + while (rs.next()) { + long price = rs.getLong("price"); + if (rs.wasNull()) { + System.out.println("Price is NULL"); + } else { + System.out.println("Price: " + price); + } + } + } + } + } + // end::null-handling[] + } + + public static void metadata() throws SQLException { + // tag::metadata[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM products")) { + ResultSetMetaData metaData = rs.getMetaData(); + int columnCount = metaData.getColumnCount(); + + for (int i = 1; i <= columnCount; i++) { + String columnName = metaData.getColumnName(i); + String columnType = metaData.getColumnTypeName(i); + System.out.println(columnName + " (" + columnType + ")"); + } + } + } + } + // end::metadata[] + } + + public static void errorHandling() throws SQLException { + // tag::error-handling[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("INSERT INTO products (id, name) VALUES (1, 'Widget')"); + } + } catch (SQLException e) { + System.err.println("SQL Error: " + e.getMessage()); + System.err.println("SQL State: " + e.getSQLState()); + System.err.println("Error Code: " + e.getErrorCode()); + + // Handle specific error conditions + if (e.getSQLState().startsWith("23")) { + System.err.println("Integrity constraint violation"); + } + } + // end::error-handling[] + } + + public static void databaseMetadata() throws SQLException { + // tag::database-metadata[] + try (Connection conn = DriverManager.getConnection(url)) { + DatabaseMetaData metaData = conn.getMetaData(); + + System.out.println("Database: " + metaData.getDatabaseProductName()); + System.out.println("Driver: " + metaData.getDriverName()); + System.out.println("Driver Version: " + metaData.getDriverVersion()); + + // List tables + try (ResultSet tables = metaData.getTables(null, null, "%", new String[]{"TABLE"})) { + while (tables.next()) { + System.out.println("Table: " + tables.getString("TABLE_NAME")); + } + } + } + // end::database-metadata[] + } +} diff --git a/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/ComplexTypesEmbedded.java b/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/ComplexTypesEmbedded.java new file mode 100644 index 0000000000..8692a320da --- /dev/null +++ b/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/ComplexTypesEmbedded.java @@ -0,0 +1,195 @@ +/* + * ComplexTypesEmbedded.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2025 Apple Inc. and the FoundationDB project 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.apple.foundationdb.relational.jdbc.examples; + +import com.apple.foundationdb.relational.api.EmbeddedRelationalArray; +import com.apple.foundationdb.relational.api.EmbeddedRelationalStruct; +import com.apple.foundationdb.relational.api.RelationalArray; +import com.apple.foundationdb.relational.api.RelationalPreparedStatement; +import com.apple.foundationdb.relational.api.RelationalResultSet; +import com.apple.foundationdb.relational.api.RelationalStruct; + +import java.sql.*; + +/** + * Example demonstrating STRUCTs and ARRAYs with the embedded driver. + * This example is from the Advanced JDBC Features documentation. + */ +public class ComplexTypesEmbedded { + private static final String CATALOG_URL = "jdbc:embed:/__SYS?schema=CATALOG"; + private static final String APP_URL = "jdbc:embed:/FRL/shop?schema=SHOP"; + + public static void main(String[] args) { + try { + setupDatabase(); + insertData(); + queryData(); + System.out.println("\nAll operations completed successfully!"); + } catch (SQLException e) { + e.printStackTrace(); + System.exit(1); + } + } + + // tag::setup[] + private static void setupDatabase() throws SQLException { + try (Connection conn = DriverManager.getConnection(CATALOG_URL)) { + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("DROP DATABASE IF EXISTS \"/FRL/shop\""); + stmt.executeUpdate("DROP SCHEMA TEMPLATE IF EXISTS shop_template"); + + stmt.executeUpdate( + "CREATE SCHEMA TEMPLATE shop_template " + + "CREATE TABLE orders (" + + " id BIGINT PRIMARY KEY, " + + " customer STRUCT>, " + + " items ARRAY>, " + + " tags ARRAY" + + ")" + ); + + stmt.executeUpdate("CREATE DATABASE \"/FRL/shop\""); + stmt.executeUpdate("CREATE SCHEMA \"/FRL/shop/SHOP\" WITH TEMPLATE shop_template"); + + System.out.println("Database and schema created successfully"); + } + } + } + // end::setup[] + + // tag::insert-struct[] + private static void insertData() throws SQLException { + try (Connection conn = DriverManager.getConnection(APP_URL)) { + conn.setAutoCommit(false); + + String sql = "INSERT INTO orders (id, customer, items, tags) " + + "VALUES (?, ?, ?, ?)"; + try (RelationalPreparedStatement pstmt = + conn.prepareStatement(sql).unwrap(RelationalPreparedStatement.class)) { + + pstmt.setLong(1, 1L); + + // Create a STRUCT with nested STRUCT for customer + RelationalStruct customer = EmbeddedRelationalStruct.newBuilder() + .addString("name", "Alice Johnson") + .addString("email", "alice@example.com") + .addStruct("address", EmbeddedRelationalStruct.newBuilder() + .addString("street", "123 Main St") + .addString("city", "Springfield") + .build()) + .build(); + + pstmt.setObject(2, customer); + + // Create array of STRUCT values + pstmt.setArray(3, EmbeddedRelationalArray.newBuilder() + .addAll( + EmbeddedRelationalStruct.newBuilder() + .addInt("product_id", 101) + .addString("name", "Laptop") + .addInt("quantity", 1) + .build(), + EmbeddedRelationalStruct.newBuilder() + .addInt("product_id", 202) + .addString("name", "Mouse") + .addInt("quantity", 2) + .build() + ) + .build()); + + // Create simple string array + pstmt.setArray(4, EmbeddedRelationalArray.newBuilder() + .addAll("electronics", "urgent", "gift") + .build()); + + pstmt.executeUpdate(); + conn.commit(); + System.out.println("Data inserted successfully"); + } + } + } + // end::insert-struct[] + + // tag::query-struct[] + private static void queryData() throws SQLException { + try (Connection conn = DriverManager.getConnection(APP_URL)) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM orders WHERE id = 1")) { + if (rs.next()) { + long id = rs.getLong("id"); + System.out.printf("Order %d:%n", id); + + // Read customer STRUCT + RelationalResultSet relationalRs = rs.unwrap(RelationalResultSet.class); + RelationalStruct customer = relationalRs.getStruct("customer"); + if (customer != null) { + String name = customer.getString("name"); + String email = customer.getString("email"); + + System.out.printf(" Customer: %s (%s)%n", name, email); + + // Access nested address STRUCT + RelationalStruct address = customer.getStruct("address"); + if (address != null) { + String street = address.getString("street"); + String city = address.getString("city"); + System.out.printf(" Address: %s, %s%n", street, city); + } + } + + // Process items array + Array itemsArray = rs.getArray("items"); + if (itemsArray != null) { + System.out.println(" Items:"); + try (ResultSet arrayRs = itemsArray.getResultSet()) { + // Unwrap to RelationalResultSet to access getStruct() + RelationalResultSet itemsRelRs = arrayRs.unwrap(RelationalResultSet.class); + + while (itemsRelRs.next()) { + int index = itemsRelRs.getInt(1); + RelationalStruct item = itemsRelRs.getStruct(2); + + int productId = item.getInt("product_id"); + String itemName = item.getString("name"); + int quantity = item.getInt("quantity"); + + System.out.printf(" - %s (ID: %d) x %d%n", + itemName, productId, quantity); + } + } + } + + // Process tags array + Array tagsArray = rs.getArray("tags"); + if (tagsArray != null) { + System.out.println(" Tags:"); + Object[] tags = (Object[]) tagsArray.getArray(); + for (Object tag : tags) { + System.out.println(" - " + tag); + } + } + } + } + } + } + } + // end::query-struct[] +} diff --git a/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippets.java b/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippets.java new file mode 100644 index 0000000000..40031b42c7 --- /dev/null +++ b/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippets.java @@ -0,0 +1,397 @@ +/* + * DirectAccessSnippets.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2025 Apple Inc. and the FoundationDB project 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.apple.foundationdb.relational.jdbc.examples; + +import com.apple.foundationdb.relational.api.Continuation; +import com.apple.foundationdb.relational.api.EmbeddedRelationalStruct; +import com.apple.foundationdb.relational.api.KeySet; +import com.apple.foundationdb.relational.api.Options; +import com.apple.foundationdb.relational.api.RelationalResultSet; +import com.apple.foundationdb.relational.api.RelationalStatement; +import com.apple.foundationdb.relational.api.RelationalStruct; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Code snippets for JDBC Key-Based Data Access documentation. + * This class is not meant to be run, but contains tagged sections referenced by the documentation. + */ +public class DirectAccessSnippets { + private static final String url = "jdbc:embed:/FRL/shop?schema=SHOP"; + + public static void unwrapStatement() throws SQLException { + // tag::unwrap-statement[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + // Unwrap to RelationalStatement to access direct access methods + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Now you can use executeScan, executeGet, executeInsert, executeDelete + } + } + // end::unwrap-statement[] + } + + public static void scanBasic() throws SQLException { + // tag::scan-basic[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Scan all records in the products table + KeySet emptyKey = new KeySet(); + try (RelationalResultSet rs = relStmt.executeScan("products", emptyKey, Options.NONE)) { + while (rs.next()) { + long id = rs.getLong("id"); + String name = rs.getString("name"); + System.out.println("Product: " + id + " - " + name); + } + } + } + } + // end::scan-basic[] + } + + public static void scanWithPrefix() throws SQLException { + // tag::scan-prefix[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Scan products with a specific category (assuming category is part of the key) + KeySet keyPrefix = new KeySet() + .setKeyColumn("category", "Electronics"); + + try (RelationalResultSet rs = relStmt.executeScan("products", keyPrefix, Options.NONE)) { + while (rs.next()) { + String name = rs.getString("name"); + long price = rs.getLong("price"); + System.out.println(name + ": $" + price); + } + } + } + } + // end::scan-prefix[] + } + + public static void scanWithContinuation() throws SQLException { + // tag::scan-continuation[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + KeySet keyPrefix = new KeySet(); + int batchSize = 10; + Continuation continuation = null; + + // Initial scan without continuation + Options options = Options.builder() + .withOption(Options.Name.MAX_ROWS, batchSize) + .build(); + + int totalRecords = 0; + try (RelationalResultSet rs = relStmt.executeScan("products", keyPrefix, options)) { + while (rs.next()) { + // Process first batch + System.out.println("Product: " + rs.getString("name")); + totalRecords++; + } + continuation = rs.getContinuation(); + } + + // Continue scanning with continuation + while (!continuation.atEnd()) { + Options contOptions = Options.builder() + .withOption(Options.Name.CONTINUATION, continuation) + .withOption(Options.Name.MAX_ROWS, batchSize) + .build(); + + int rowCount = 0; + try (RelationalResultSet rs = relStmt.executeScan("products", keyPrefix, contOptions)) { + while (rs.next()) { + // Process record + System.out.println("Product: " + rs.getString("name")); + rowCount++; + } + + // Get continuation for next batch + continuation = rs.getContinuation(); + } + + totalRecords += rowCount; + System.out.println("Processed " + rowCount + " records in this batch"); + } + + System.out.println("Total records processed: " + totalRecords); + } + } + // end::scan-continuation[] + } + + public static void getByKey() throws SQLException { + // tag::get-basic[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Get a single record by primary key + KeySet primaryKey = new KeySet() + .setKeyColumn("id", 1L); + + try (RelationalResultSet rs = relStmt.executeGet("products", primaryKey, Options.NONE)) { + if (rs.next()) { + String name = rs.getString("name"); + long price = rs.getLong("price"); + System.out.println("Found: " + name + " - $" + price); + } else { + System.out.println("Product not found"); + } + } + } + } + // end::get-basic[] + } + + public static void getCompositeKey() throws SQLException { + // tag::get-composite[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Get with composite primary key + KeySet primaryKey = new KeySet() + .setKeyColumn("store_id", 100L) + .setKeyColumn("product_id", 1L); + + try (RelationalResultSet rs = relStmt.executeGet("inventory", primaryKey, Options.NONE)) { + if (rs.next()) { + int stock = rs.getInt("quantity"); + System.out.println("Stock level: " + stock); + } + } + } + } + // end::get-composite[] + } + + public static void insertSingle() throws SQLException { + // tag::insert-single[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Build a record to insert + RelationalStruct product = EmbeddedRelationalStruct.newBuilder() + .addLong("id", 100L) + .addString("name", "New Widget") + .addString("category", "Electronics") + .addLong("price", 299L) + .addInt("stock", 50) + .build(); + + int rowsInserted = relStmt.executeInsert("products", product); + System.out.println("Inserted " + rowsInserted + " row(s)"); + + conn.commit(); + } catch (SQLException e) { + conn.rollback(); + throw e; + } + } + // end::insert-single[] + } + + public static void insertBatch() throws SQLException { + // tag::insert-batch[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Build multiple records + List products = new ArrayList<>(); + + products.add(EmbeddedRelationalStruct.newBuilder() + .addLong("id", 101L) + .addString("name", "Widget A") + .addString("category", "Electronics") + .addLong("price", 199L) + .addInt("stock", 25) + .build()); + + products.add(EmbeddedRelationalStruct.newBuilder() + .addLong("id", 102L) + .addString("name", "Widget B") + .addString("category", "Electronics") + .addLong("price", 249L) + .addInt("stock", 30) + .build()); + + int rowsInserted = relStmt.executeInsert("products", products); + System.out.println("Inserted " + rowsInserted + " row(s)"); + + conn.commit(); + } catch (SQLException e) { + conn.rollback(); + throw e; + } + } + // end::insert-batch[] + } + + public static void insertWithReplace() throws SQLException { + // tag::insert-replace[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + RelationalStruct product = EmbeddedRelationalStruct.newBuilder() + .addLong("id", 1L) + .addString("name", "Updated Widget") + .addString("category", "Electronics") + .addLong("price", 199L) + .addInt("stock", 100) + .build(); + + // Replace if the primary key already exists + Options options = Options.builder() + .withOption(Options.Name.REPLACE_ON_DUPLICATE_PK, true) + .build(); + + int rowsInserted = relStmt.executeInsert("products", product, options); + System.out.println("Inserted/replaced " + rowsInserted + " row(s)"); + + conn.commit(); + } catch (SQLException e) { + conn.rollback(); + throw e; + } + } + // end::insert-replace[] + } + + public static void deleteSingle() throws SQLException { + // tag::delete-single[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Delete by primary key + KeySet primaryKey = new KeySet() + .setKeyColumn("id", 100L); + + List keysToDelete = List.of(primaryKey); + int rowsDeleted = relStmt.executeDelete("products", keysToDelete); + System.out.println("Deleted " + rowsDeleted + " row(s)"); + + conn.commit(); + } catch (SQLException e) { + conn.rollback(); + throw e; + } + } + // end::delete-single[] + } + + public static void deleteBatch() throws SQLException { + // tag::delete-batch[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Delete multiple records by their keys + List keysToDelete = List.of( + new KeySet().setKeyColumn("id", 101L), + new KeySet().setKeyColumn("id", 102L), + new KeySet().setKeyColumn("id", 103L) + ); + + int rowsDeleted = relStmt.executeDelete("products", keysToDelete); + System.out.println("Deleted " + rowsDeleted + " row(s)"); + + conn.commit(); + } catch (SQLException e) { + conn.rollback(); + throw e; + } + } + // end::delete-batch[] + } + + public static void deleteRange() throws SQLException { + // tag::delete-range[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Delete all products in a specific category (assuming category is part of key) + KeySet keyPrefix = new KeySet() + .setKeyColumn("category", "Discontinued"); + + relStmt.executeDeleteRange("products", keyPrefix, Options.NONE); + System.out.println("Deleted all discontinued products"); + + conn.commit(); + } catch (SQLException e) { + conn.rollback(); + throw e; + } + } + // end::delete-range[] + } + + public static void withIndexHint() throws SQLException { + // tag::index-hint[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Use a specific index for the scan + Options options = Options.builder() + .withOption(Options.Name.INDEX_HINT, "products_by_category") + .build(); + + KeySet keyPrefix = new KeySet() + .setKeyColumn("category", "Electronics"); + + try (RelationalResultSet rs = relStmt.executeScan("products", keyPrefix, options)) { + while (rs.next()) { + System.out.println("Product: " + rs.getString("name")); + } + } + } + } + // end::index-hint[] + } +} diff --git a/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippetsServer.java b/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippetsServer.java new file mode 100644 index 0000000000..5d071470d6 --- /dev/null +++ b/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/DirectAccessSnippetsServer.java @@ -0,0 +1,397 @@ +/* + * DirectAccessSnippetsServer.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2025 Apple Inc. and the FoundationDB project 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.apple.foundationdb.relational.jdbc.examples; + +import com.apple.foundationdb.relational.api.Continuation; +import com.apple.foundationdb.relational.api.KeySet; +import com.apple.foundationdb.relational.api.Options; +import com.apple.foundationdb.relational.api.RelationalResultSet; +import com.apple.foundationdb.relational.api.RelationalStatement; +import com.apple.foundationdb.relational.api.RelationalStruct; +import com.apple.foundationdb.relational.jdbc.JDBCRelationalStruct; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Code snippets for JDBC Key-Based Data Access documentation using the server driver. + * This class is not meant to be run, but contains tagged sections referenced by the documentation. + */ +public class DirectAccessSnippetsServer { + private static final String url = "jdbc:relational://localhost:7243/FRL/shop?schema=SHOP"; + + public static void unwrapStatement() throws SQLException { + // tag::unwrap-statement[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + // Unwrap to RelationalStatement to access direct access methods + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Now you can use executeScan, executeGet, executeInsert, executeDelete + } + } + // end::unwrap-statement[] + } + + public static void scanBasic() throws SQLException { + // tag::scan-basic[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Scan all records in the products table + KeySet emptyKey = new KeySet(); + try (RelationalResultSet rs = relStmt.executeScan("products", emptyKey, Options.NONE)) { + while (rs.next()) { + long id = rs.getLong("id"); + String name = rs.getString("name"); + System.out.println("Product: " + id + " - " + name); + } + } + } + } + // end::scan-basic[] + } + + public static void scanWithPrefix() throws SQLException { + // tag::scan-prefix[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Scan products with a specific category (assuming category is part of the key) + KeySet keyPrefix = new KeySet() + .setKeyColumn("category", "Electronics"); + + try (RelationalResultSet rs = relStmt.executeScan("products", keyPrefix, Options.NONE)) { + while (rs.next()) { + String name = rs.getString("name"); + long price = rs.getLong("price"); + System.out.println(name + ": $" + price); + } + } + } + } + // end::scan-prefix[] + } + + public static void scanWithContinuation() throws SQLException { + // tag::scan-continuation[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + KeySet keyPrefix = new KeySet(); + int batchSize = 10; + Continuation continuation = null; + + // Initial scan without continuation + Options options = Options.builder() + .withOption(Options.Name.MAX_ROWS, batchSize) + .build(); + + int totalRecords = 0; + try (RelationalResultSet rs = relStmt.executeScan("products", keyPrefix, options)) { + while (rs.next()) { + // Process first batch + System.out.println("Product: " + rs.getString("name")); + totalRecords++; + } + continuation = rs.getContinuation(); + } + + // Continue scanning with continuation + while (!continuation.atEnd()) { + Options contOptions = Options.builder() + .withOption(Options.Name.CONTINUATION, continuation) + .withOption(Options.Name.MAX_ROWS, batchSize) + .build(); + + int rowCount = 0; + try (RelationalResultSet rs = relStmt.executeScan("products", keyPrefix, contOptions)) { + while (rs.next()) { + // Process record + System.out.println("Product: " + rs.getString("name")); + rowCount++; + } + + // Get continuation for next batch + continuation = rs.getContinuation(); + } + + totalRecords += rowCount; + System.out.println("Processed " + rowCount + " records in this batch"); + } + + System.out.println("Total records processed: " + totalRecords); + } + } + // end::scan-continuation[] + } + + public static void getByKey() throws SQLException { + // tag::get-basic[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Get a single record by primary key + KeySet primaryKey = new KeySet() + .setKeyColumn("id", 1L); + + try (RelationalResultSet rs = relStmt.executeGet("products", primaryKey, Options.NONE)) { + if (rs.next()) { + String name = rs.getString("name"); + long price = rs.getLong("price"); + System.out.println("Found: " + name + " - $" + price); + } else { + System.out.println("Product not found"); + } + } + } + } + // end::get-basic[] + } + + public static void getCompositeKey() throws SQLException { + // tag::get-composite[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Get with composite primary key + KeySet primaryKey = new KeySet() + .setKeyColumn("store_id", 100L) + .setKeyColumn("product_id", 1L); + + try (RelationalResultSet rs = relStmt.executeGet("inventory", primaryKey, Options.NONE)) { + if (rs.next()) { + int stock = rs.getInt("quantity"); + System.out.println("Stock level: " + stock); + } + } + } + } + // end::get-composite[] + } + + public static void insertSingle() throws SQLException { + // tag::insert-single[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Build a record to insert + RelationalStruct product = JDBCRelationalStruct.newBuilder() + .addLong("id", 100L) + .addString("name", "New Widget") + .addString("category", "Electronics") + .addLong("price", 299L) + .addInt("stock", 50) + .build(); + + int rowsInserted = relStmt.executeInsert("products", product); + System.out.println("Inserted " + rowsInserted + " row(s)"); + + conn.commit(); + } catch (SQLException e) { + conn.rollback(); + throw e; + } + } + // end::insert-single[] + } + + public static void insertBatch() throws SQLException { + // tag::insert-batch[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Build multiple records + List products = new ArrayList<>(); + + products.add(JDBCRelationalStruct.newBuilder() + .addLong("id", 101L) + .addString("name", "Widget A") + .addString("category", "Electronics") + .addLong("price", 199L) + .addInt("stock", 25) + .build()); + + products.add(JDBCRelationalStruct.newBuilder() + .addLong("id", 102L) + .addString("name", "Widget B") + .addString("category", "Electronics") + .addLong("price", 249L) + .addInt("stock", 30) + .build()); + + int rowsInserted = relStmt.executeInsert("products", products); + System.out.println("Inserted " + rowsInserted + " row(s)"); + + conn.commit(); + } catch (SQLException e) { + conn.rollback(); + throw e; + } + } + // end::insert-batch[] + } + + public static void insertWithReplace() throws SQLException { + // tag::insert-replace[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + RelationalStruct product = JDBCRelationalStruct.newBuilder() + .addLong("id", 1L) + .addString("name", "Updated Widget") + .addString("category", "Electronics") + .addLong("price", 199L) + .addInt("stock", 100) + .build(); + + // Replace if the primary key already exists + Options options = Options.builder() + .withOption(Options.Name.REPLACE_ON_DUPLICATE_PK, true) + .build(); + + int rowsInserted = relStmt.executeInsert("products", product, options); + System.out.println("Inserted/replaced " + rowsInserted + " row(s)"); + + conn.commit(); + } catch (SQLException e) { + conn.rollback(); + throw e; + } + } + // end::insert-replace[] + } + + public static void deleteSingle() throws SQLException { + // tag::delete-single[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Delete by primary key + KeySet primaryKey = new KeySet() + .setKeyColumn("id", 100L); + + List keysToDelete = List.of(primaryKey); + int rowsDeleted = relStmt.executeDelete("products", keysToDelete); + System.out.println("Deleted " + rowsDeleted + " row(s)"); + + conn.commit(); + } catch (SQLException e) { + conn.rollback(); + throw e; + } + } + // end::delete-single[] + } + + public static void deleteBatch() throws SQLException { + // tag::delete-batch[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Delete multiple records by their keys + List keysToDelete = List.of( + new KeySet().setKeyColumn("id", 101L), + new KeySet().setKeyColumn("id", 102L), + new KeySet().setKeyColumn("id", 103L) + ); + + int rowsDeleted = relStmt.executeDelete("products", keysToDelete); + System.out.println("Deleted " + rowsDeleted + " row(s)"); + + conn.commit(); + } catch (SQLException e) { + conn.rollback(); + throw e; + } + } + // end::delete-batch[] + } + + public static void deleteRange() throws SQLException { + // tag::delete-range[] + try (Connection conn = DriverManager.getConnection(url)) { + conn.setAutoCommit(false); + + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Delete all products in a specific category (assuming category is part of key) + KeySet keyPrefix = new KeySet() + .setKeyColumn("category", "Discontinued"); + + relStmt.executeDeleteRange("products", keyPrefix, Options.NONE); + System.out.println("Deleted all discontinued products"); + + conn.commit(); + } catch (SQLException e) { + conn.rollback(); + throw e; + } + } + // end::delete-range[] + } + + public static void withIndexHint() throws SQLException { + // tag::index-hint[] + try (Connection conn = DriverManager.getConnection(url)) { + try (Statement stmt = conn.createStatement()) { + RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class); + + // Use a specific index for the scan + Options options = Options.builder() + .withOption(Options.Name.INDEX_HINT, "products_by_category") + .build(); + + KeySet keyPrefix = new KeySet() + .setKeyColumn("category", "Electronics"); + + try (RelationalResultSet rs = relStmt.executeScan("products", keyPrefix, options)) { + while (rs.next()) { + System.out.println("Product: " + rs.getString("name")); + } + } + } + } + // end::index-hint[] + } +} diff --git a/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/ProductManagerEmbedded.java b/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/ProductManagerEmbedded.java new file mode 100644 index 0000000000..eafb679b82 --- /dev/null +++ b/examples/src/main/java/com/apple/foundationdb/relational/jdbc/examples/ProductManagerEmbedded.java @@ -0,0 +1,155 @@ +/* + * ProductManagerEmbedded.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2025 Apple Inc. and the FoundationDB project 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.apple.foundationdb.relational.jdbc.examples; + +import java.sql.*; + +// tag::complete[] +/** + * Example demonstrating basic JDBC operations with the embedded driver. + * This example is from the JDBC Guide documentation. + */ +public class ProductManagerEmbedded { + private static final String CATALOG_URL = "jdbc:embed:/__SYS?schema=CATALOG"; + private static final String APP_URL = "jdbc:embed:/FRL/shop?schema=SHOP"; + + public static void main(String[] args) { + try { + setupDatabase(); + insertProducts(); + queryProducts(); + updateProduct(); + deleteProduct(); + System.out.println("All operations completed successfully!"); + } catch (SQLException e) { + e.printStackTrace(); + System.exit(1); + } + } + + private static void setupDatabase() throws SQLException { + // Connect to CATALOG to create database objects + try (Connection conn = DriverManager.getConnection(CATALOG_URL)) { + try (Statement stmt = conn.createStatement()) { + // Drop existing objects if they exist + stmt.executeUpdate("DROP DATABASE IF EXISTS \"/FRL/shop\""); + stmt.executeUpdate("DROP SCHEMA TEMPLATE IF EXISTS shop_template"); + + // Create schema template with table definition + stmt.executeUpdate( + "CREATE SCHEMA TEMPLATE shop_template " + + "CREATE TABLE products (" + + " id BIGINT PRIMARY KEY, " + + " name STRING, " + + " category STRING, " + + " price BIGINT, " + + " stock INTEGER" + + ")" + ); + + // Create database and schema + stmt.executeUpdate("CREATE DATABASE \"/FRL/shop\""); + stmt.executeUpdate("CREATE SCHEMA \"/FRL/shop/SHOP\" WITH TEMPLATE shop_template"); + + System.out.println("Database and schema created successfully"); + } + } + } + + private static void insertProducts() throws SQLException { + String sql = "INSERT INTO products (id, name, category, price, stock) VALUES (?, ?, ?, ?, ?)"; + + try (Connection conn = DriverManager.getConnection(APP_URL)) { + try (PreparedStatement pstmt = conn.prepareStatement(sql)) { + conn.setAutoCommit(false); + + Object[][] products = { + {1L, "Widget A", "Electronics", 100L, 50}, + {2L, "Widget B", "Electronics", 150L, 30}, + {3L, "Gadget X", "Electronics", 200L, 20} + }; + + for (Object[] product : products) { + pstmt.setLong(1, (Long) product[0]); + pstmt.setString(2, (String) product[1]); + pstmt.setString(3, (String) product[2]); + pstmt.setLong(4, (Long) product[3]); + pstmt.setInt(5, (Integer) product[4]); + pstmt.executeUpdate(); + } + + conn.commit(); + System.out.println("Products inserted successfully"); + } + } + } + + private static void queryProducts() throws SQLException { + String sql = "SELECT * FROM products WHERE category = ? ORDER BY price"; + + try (Connection conn = DriverManager.getConnection(APP_URL)) { + try (PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setString(1, "Electronics"); + + try (ResultSet rs = pstmt.executeQuery()) { + System.out.println("\nProducts in Electronics category:"); + while (rs.next()) { + System.out.printf(" %d: %s - $%d (Stock: %d)%n", + rs.getLong("id"), + rs.getString("name"), + rs.getLong("price"), + rs.getInt("stock") + ); + } + } + } + } + } + + private static void updateProduct() throws SQLException { + String sql = "UPDATE products SET price = ?, stock = ? WHERE id = ?"; + + try (Connection conn = DriverManager.getConnection(APP_URL)) { + try (PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setLong(1, 120L); + pstmt.setInt(2, 45); + pstmt.setLong(3, 1L); + + int rowsAffected = pstmt.executeUpdate(); + System.out.println("\nUpdated " + rowsAffected + " product(s)"); + } + } + } + + private static void deleteProduct() throws SQLException { + String sql = "DELETE FROM products WHERE id = ?"; + + try (Connection conn = DriverManager.getConnection(APP_URL)) { + try (PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setLong(1, 3L); + + int rowsAffected = pstmt.executeUpdate(); + System.out.println("Deleted " + rowsAffected + " product(s)"); + } + } + } +} +// end::complete[]