JDBC data sources
SOURCE_JDBC() connects FeatureQL to any JDBC-compatible database — PostgreSQL, MySQL, Oracle, SQL Server, and others. This gives your real-time features direct access to relational data without building custom data-fetching services.
Creating a JDBC source
Establish a connection using a standard JDBC connection string:
CREATE OR REPLACE FEATURE FM.TUTORIALS.EXTERNAL_JDBC.POSTGRES_CONN AS
SOURCE_JDBC(
'postgresql://featuremesh:featuremesh@host.docker.internal:5433/featuremesh?sslmode=disable'
WITH (
tables=ARRAY['serving_slt_customers'],
timeout='500ms'
)
);| feature_name VARCHAR | status VARCHAR | message VARCHAR |
|---|---|---|
| FM.TUTORIALS.EXTERNAL_JDBC.POSTGRES_CONN | CREATED | Feature created as not exists |
The tables array acts as an allowlist — only the listed tables and views are accessible through this connection. This is both a security measure and a documentation aid: anyone reading the feature definition can see exactly which tables it touches.
In a local development process, synchronously install the persisted connection before querying it:
REFRESH FEATURES FM.TUTORIALS.EXTERNAL_JDBC.POSTGRES_CONN;| FEATURE VARCHAR | KIND VARCHAR | STATUS VARCHAR | MESSAGE VARCHAR |
|---|---|---|---|
| FM.TUTORIALS.EXTERNAL_JDBC.POSTGRES_CONN | SOURCE_JDBC | REFRESHED | (empty) |
The refresh returns only after the DataFusion catalog can resolve the configured table.
Querying with EXTERNAL_COLUMNS()
The most straightforward way to access JDBC data is EXTERNAL_COLUMNS() with a VIEW() reference. This maps database columns directly to feature fields, just like batch data source mappings:
WITH
CUSTOMER_ID := INPUT(BIGINT),
POSTGRES_SRC := EXTERNAL_COLUMNS(
customer_id BIGINT BIND TO CUSTOMER_ID,
last_order_id BIGINT,
status VARCHAR
FROM VIEW(FM.TUTORIALS.EXTERNAL_JDBC.POSTGRES_CONN[serving_slt_customers])
),
SELECT
CUSTOMER_ID := BIND_VALUES(ARRAY[1, 2]),
LAST_ORDER_ID := POSTGRES_SRC[last_order_id],
STATUS := POSTGRES_SRC[status]
;| CUSTOMER_ID BIGINT | LAST_ORDER_ID BIGINT | STATUS VARCHAR |
|---|---|---|
| 1 | 101 | active |
| 2 | 202 | inactive |
The BIND TO CUSTOMER_ID clause links the customer_id column to the input feature, so FeatureMesh knows how to parameterize the query.
Querying with EXTERNAL_VIEW()
For more control over the SQL sent to the database, use EXTERNAL_VIEW() with an explicit query:
WITH
CUSTOMER_ID := INPUT(BIGINT),
POSTGRES_SRC := EXTERNAL_VIEW(
`SELECT customer_id, last_order_id, status
FROM %FM.TUTORIALS.EXTERNAL_JDBC.POSTGRES_CONN[serving_slt_customers]`
ON `SELF.customer_id=%CUSTOMER_ID`
AS ROW(customer_id BIGINT, last_order_id BIGINT, status VARCHAR)
),
SELECT
CUSTOMER_ID := BIND_VALUES(ARRAY[1, 2]),
LAST_ORDER_ID := POSTGRES_SRC[last_order_id],
STATUS := POSTGRES_SRC[status]
;| CUSTOMER_ID BIGINT | LAST_ORDER_ID BIGINT | STATUS VARCHAR |
|---|---|---|
| 1 | 101 | active |
| 2 | 202 | inactive |
The %FM.TUTORIALS.EXTERNAL_JDBC.POSTGRES_CONN[serving_slt_customers] placeholder resolves to the actual table reference, and %CUSTOMER_ID binds the input. This approach is useful when you need to select specific columns or apply database-side transformations.
The connection reads current database state. Updating customer 2 in PostgreSQL changes the next serving result without recreating the source:
WITH
CUSTOMER_ID := INPUT(BIGINT),
POSTGRES_SRC := EXTERNAL_COLUMNS(
customer_id BIGINT BIND TO CUSTOMER_ID,
last_order_id BIGINT,
status VARCHAR
FROM VIEW(FM.TUTORIALS.EXTERNAL_JDBC.POSTGRES_CONN[serving_slt_customers])
),
SELECT
CUSTOMER_ID := BIND_VALUE(2),
LAST_ORDER_ID := POSTGRES_SRC[last_order_id],
STATUS := POSTGRES_SRC[status]
;| CUSTOMER_ID BIGINT | LAST_ORDER_ID BIGINT | STATUS VARCHAR |
|---|---|---|
| 2 | 303 | active |
Best practices
Reuse connections: Create one SOURCE_JDBC() per database and share it across features. Connection pooling happens automatically.
Be precise with types: Specify exact types in ROW() specifications. Type mismatches between the database and FeatureQL cause runtime errors, not compile-time warnings.
Watch for load: FeatureMesh can generate bursts of concurrent queries when serving many requests. Monitor your database load and consider connection limits, read replicas, or caching layers if needed.
Index your join columns: The columns used in BIND TO or ON clauses become WHERE conditions in the generated SQL. Make sure they're indexed in the source database.