Skip to main content

🧠 Flinkflow SQL & ML Bridge

Flinkflow extends its YAML DSL with two powerful step types — sql and ml — that bridge Apache Flink's Table/SQL API and Flink ML library into the declarative pipeline model. This guide explains what these features are, how to use them, and how they compare to writing native Flink SQL or Flink ML code.


📐 Architecture: The DataStream ↔ Table Bridge

Flinkflow's core data model is DataStream<String> — every record flowing through the pipeline is a JSON string (see ADR-001). The SQL and ML steps work by transparently converting between this string-based model and Flink's typed Table / Row representation:

A single shared StreamTableEnvironment is created once per pipeline execution and reused across all SQL and ML steps. This enables:

  • Multiple SQL steps in the same pipeline to reference each other's temporary views
  • ML steps and SQL steps to coexist and share the same Table environment
  • Zero boilerplate — the conversion is handled automatically based on schema.* properties you declare in YAML

🔍 The sql Step

The sql step lets you write standard ANSI SQL queries against your streaming data — directly in your pipeline YAML.

Single-Source SQL

When a SQL step follows a single upstream source, the incoming DataStream<String> is automatically registered as a temporary table view:

name: "Simple SQL Filter"
parallelism: 1
steps:
- type: source
name: events
connector: kafka-source
properties:
bootstrap.servers: "kafka:9092"
topic: "raw-events"

- type: sql
name: high-value-filter
properties:
schema.userId: "string"
schema.eventType: "string"
schema.amount: "double"
tableName: "events"
query: |
SELECT userId, eventType, amount
FROM events
WHERE amount > 100.0

- type: sink
name: console-sink

Key properties:

PropertyRequiredDescription
schema.<field>Declares the field name and type for JSON → Row mapping
queryThe SQL query to execute (can also be placed in code:)
tableNameName of the temporary view (defaults to "input")
outputModeappend, changelog, or auto (default)

Multi-Source SQL (JOINs)

The real power unlocks when you need to join or combine multiple streams. Use the inputs list to reference multiple upstream steps by name, with namespaced schemas:

name: "Order Enrichment Pipeline"
parallelism: 1
steps:
- type: source
name: orders
connector: kafka-source
properties:
bootstrap.servers: "kafka:9092"
topic: "orders"

- type: source
name: customers
connector: kafka-source
properties:
bootstrap.servers: "kafka:9092"
topic: "customers"

- type: sql
name: enrich-orders
inputs: [orders, customers]
properties:
# Namespaced schemas: schema.<tableName>.<field>
schema.orders.orderId: "string"
schema.orders.customerId: "string"
schema.orders.amount: "double"
schema.orders.orderTime: "timestamp"

schema.customers.customerId: "string"
schema.customers.name: "string"
schema.customers.level: "string"

query: |
SELECT
o.orderId,
c.name AS customer_name,
c.level AS customer_level,
o.amount
FROM orders o
JOIN customers c ON o.customerId = c.customerId

- type: sink
name: console-sink

[!IMPORTANT] When using inputs, every input stream name must have a matching set of schema.<tableName>.<field> entries. The engine validates this at startup and will fail fast with a clear error message if any input is missing its schema.

Supported Schema Types

The schema.* property values map to Flink type system types:

YAML TypeFlink TypeInformationExample
stringTypes.STRINGschema.name: "string"
int / integerTypes.INTschema.age: "int"
longTypes.LONGschema.timestamp: "long"
double / floatTypes.DOUBLEschema.price: "double"
booleanTypes.BOOLEANschema.active: "boolean"
timestampTypes.LOCAL_DATE_TIMEschema.eventTime: "timestamp"
dateTypes.LOCAL_DATEschema.birthDate: "date"
decimalTypes.BIG_DECschema.revenue: "decimal"
vectorVectorTypeInfo.INSTANCEschema.features: "vector"
array<T>Types.LIST(T)schema.tags: "array<string>"
map<K,V>Types.MAP(K, V)schema.meta: "map<string,string>"

Event-Time & Watermarks

For windowed SQL queries (e.g., TUMBLE, HOP), you can configure event-time watermarks:

- type: sql
name: windowed-agg
properties:
schema.sensorId: "string"
schema.temperature: "double"
schema.eventTime: "timestamp"

# Watermark configuration
watermark.column: "eventTime"
watermark.delay: "5000" # 5 seconds of allowed out-of-orderness

tableName: "readings"
query: |
SELECT
sensorId,
TUMBLE_START(eventTime, INTERVAL '1' MINUTE) AS window_start,
AVG(temperature) AS avg_temp
FROM readings
GROUP BY sensorId, TUMBLE(eventTime, INTERVAL '1' MINUTE)

For multi-source pipelines, watermarks are namespaced per input:

watermark.orders.column: "orderTime"
watermark.orders.delay: "3000"

Output Modes

The outputMode property controls how the SQL result table is converted back to a DataStream<String>:

ModeBehaviourUse When
appendUses tEnv.toDataStream() — only insert rowsSimple filters, projections, non-aggregating JOINs
changelogUses tEnv.toChangelogStream() — includes _op field (+I, -D, +U, -U)Aggregations, GROUP BY, windowed queries
auto (default)Tries append first; falls back to changelog on TableExceptionMost use cases — let the engine decide

Example changelog output:

{"_op": "+I", "sensorId": "s-01", "window_start": "2026-06-13T12:00", "avg_temp": 23.5}
{"_op": "-U", "sensorId": "s-01", "window_start": "2026-06-13T12:00", "avg_temp": 23.5}
{"_op": "+U", "sensorId": "s-01", "window_start": "2026-06-13T12:00", "avg_temp": 24.1}

🤖 The ml Step

The ml step embeds native Apache Flink ML stages (Estimators and Transformers) directly in your pipeline YAML. No Java code, no Maven dependencies — just declare the algorithm and its parameters.

Basic Usage

name: "Feature Engineering Pipeline"
parallelism: 1
steps:
- type: source
name: sensor-data
connector: kafka-source
properties:
bootstrap.servers: "kafka:9092"
topic: "sensor-readings"

- type: ml
name: assemble-features
properties:
algorithm: "VectorAssembler"
inputCols: "temperature,humidity,pressure"
outputCol: "features"
schema.temperature: "double"
schema.humidity: "double"
schema.pressure: "double"

- type: ml
name: normalize
properties:
algorithm: "MinMaxScaler"
inputCol: "features"
outputCol: "scaledFeatures"
schema.temperature: "double"
schema.humidity: "double"
schema.pressure: "double"
schema.features: "vector"

- type: sink
name: console-sink

How It Works

  1. JSON → Row: The incoming DataStream<String> is mapped to DataStream<Row> using the schema.* definitions
  2. Row → Table: The Row stream is converted to a Flink Table via StreamTableEnvironment
  3. ML Stage Execution: The algorithm is dynamically instantiated using MLStageFactory and configured via reflection:
    • Transformer (e.g., VectorAssembler): Calls .transform(inputTable) directly
    • Estimator (e.g., MinMaxScaler, KMeans): Calls .fit(inputTable) to train, then .transform(inputTable) to apply the model
  4. Table → JSON: The output Table is converted back to DataStream<String> as JSON

Supported Algorithms (Short Names)

You can use short names or fully-qualified class names:

Short NameFull ClassType
VectorAssemblero.a.f.ml.feature.vectorassembler.VectorAssemblerTransformer
MinMaxScalero.a.f.ml.feature.minmaxscaler.MinMaxScalerEstimator
MinMaxScalerModelo.a.f.ml.feature.minmaxscaler.MinMaxScalerModelTransformer
KMeanso.a.f.ml.clustering.kmeans.KMeansEstimator
KMeansModelo.a.f.ml.clustering.kmeans.KMeansModelTransformer
LogisticRegressiono.a.f.ml.classification.logisticregression.LogisticRegressionEstimator
LogisticRegressionModelo.a.f.ml.classification.logisticregression.LogisticRegressionModelTransformer

[!TIP] Any Flink ML stage not in the short-name table can still be used — just specify the fully-qualified Java class name as the algorithm value.

Loading Pre-Trained Models

Use the modelPath property to load a previously saved model instead of training from scratch:

- type: ml
name: predict
properties:
algorithm: "KMeansModel"
modelPath: "s3://models/kmeans-v2"
schema.features: "vector"

Property Mapping

All properties (except algorithm, modelPath, and schema.*) are mapped to the ML stage's setter methods using reflection. For example, inputCol: "features" calls stage.setInputCol("features"). Supported parameter types include: String, String[], int, double, boolean, long, float, and Vector.


AspectNative Flink SQLFlinkflow SQL Step
LanguageJava/Scala application codeDeclarative YAML + inline SQL
SetupMaven project, StreamTableEnvironment boilerplate, TypeInformation wiringZero boilerplate — schema.* properties handle all type mapping
Schema DeclarationProgrammatic Schema.newBuilder().column(...) chainsFlat key-value pairs: schema.field: "type"
Multi-Table JOINsManually register each DataStream as a view, manage TypeInformation for eachDeclare inputs: [a, b] and namespaced schemas — the engine handles registration
WatermarksWatermarkStrategy.forBoundedOutOfOrderness(...) codewatermark.column / watermark.delay properties
Output HandlingManual toDataStream() vs toChangelogStream() decision + serializationAutomatic via outputMode: auto with built-in JSON serialization
DeploymentCompile → package JAR → deployApply YAML — hot-reloadable in Kubernetes
IntegrationStandalone Table API programComposable with all other Flinkflow steps (filters, process, ML, Camel, etc.)

What You Get For Free

With Flinkflow's SQL step, the engine handles all of the following automatically:

  • ✅ Creating and sharing the StreamTableEnvironment
  • ✅ Parsing JSON strings into typed Row objects
  • ✅ Registering DataStreams as temporary SQL views
  • ✅ Resolving Flink TypeInformation from simple type strings
  • ✅ Assigning watermarks for event-time processing
  • ✅ Converting SQL result tables back to JSON strings
  • ✅ Handling changelog semantics (retractions/updates) transparently

What You Give Up

Flinkflow's SQL step is designed for the 80% use case. For advanced scenarios, you may still need native Flink code:

  • ❌ Custom TypeSerializer implementations
  • ❌ Complex stateful processing that mixes Table API with ProcessFunction state
  • ❌ Dynamic table DDL (CREATE TABLE, CREATE CATALOG) — Flinkflow registers views, not full catalog tables
  • ❌ Flink SQL connectors (e.g., CREATE TABLE ... WITH ('connector' = 'kafka')) — Flinkflow uses its own connector model

[!NOTE] Flinkflow SQL is not a replacement for the Flink SQL CLI or Flink SQL Gateway. It is a bridge that lets you use SQL as a transformation step within a broader declarative pipeline — alongside filters, code snippets, ML stages, and Camel integrations.


AspectNative Flink MLFlinkflow ML Step
LanguageJava application codeDeclarative YAML
Stage Instantiationnew MinMaxScaler().setInputCol(...).setOutputCol(...)algorithm: "MinMaxScaler" + properties auto-mapped via reflection
Schema HandlingManual DataStream<Row> construction with TypeInformationschema.* properties — same as SQL step
Pipeline CompositionPipeline.of(stage1, stage2).fit(table)Sequential ml steps in YAML
Model Persistencemodel.save(path) / Model.load(env, path) in codemodelPath property to load pre-trained models
IntegrationStandalone ML programComposable with sources, SQL, filters, sinks — all in one YAML

Key Difference: The StreamTableEnvironment Bridge

In native Flink ML, you must manually:

  1. Create a StreamTableEnvironment
  2. Convert your DataStream to a Table (choosing between position-based and name-based Row modes)
  3. Handle ML-specific constraints (e.g., VectorAssembler requires position-based Rows)
  4. Convert the output Table back to your target DataStream type

Flinkflow handles all of this automatically — including the subtle position-based vs. name-based Row compatibility issue that frequently trips up developers working with VectorAssembler and similar stages.


🔗 Combining SQL and ML

Because both sql and ml steps share the same StreamTableEnvironment, you can chain them naturally:

name: "Feature Engineering + SQL Analytics"
parallelism: 1
steps:
- type: source
name: sensor-data
connector: kafka-source
properties:
bootstrap.servers: "kafka:9092"
topic: "sensors"

# Step 1: Assemble raw fields into a feature vector
- type: ml
name: assemble
properties:
algorithm: "VectorAssembler"
inputCols: "temperature,humidity"
outputCol: "features"
schema.sensorId: "string"
schema.temperature: "double"
schema.humidity: "double"

# Step 2: Normalize the feature vector
- type: ml
name: scale
properties:
algorithm: "MinMaxScaler"
inputCol: "features"
outputCol: "scaledFeatures"
schema.sensorId: "string"
schema.temperature: "double"
schema.humidity: "double"
schema.features: "vector"

# Step 3: Use SQL to filter and aggregate the results
- type: sql
name: analyze
properties:
schema.sensorId: "string"
schema.temperature: "double"
schema.humidity: "double"
schema.features: "vector"
schema.scaledFeatures: "vector"
tableName: "enriched"
query: |
SELECT sensorId, temperature, humidity
FROM enriched
WHERE temperature > 30.0

- type: sink
name: console-sink

🧩 DAG Topology Support

Flinkflow pipelines are directed acyclic graphs (DAGs), not just linear chains. The inputs property on any step allows you to reference multiple upstream steps, enabling fan-in topologies:

The GraphValidator ensures at pipeline validation time that:

  • Every step referenced in inputs exists and is defined before the referencing step
  • No circular dependencies exist
  • All input schemas are fully specified

📚 Further Reading

ResourceDescription
User GuideCore Flinkflow concepts and quick start
Configuration ReferenceFull DSL spec for all connectors and properties
ArchitectureHow the engine works under the hood
Vision & RoadmapStrategic direction and upcoming features
Apache Flink SQL DocsOfficial Flink SQL reference
Apache Flink ML DocsOfficial Flink ML reference

Questions or feedback? Connect with us on Zulip or open an issue.