Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 62 additions & 12 deletions docs/docs/build/connectors/olap/starrocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,21 @@ sidebar_position: 5

[StarRocks](https://www.starrocks.io/) is an open-source, high-performance analytical database designed for real-time, multi-dimensional analytics on large-scale data. It supports both primary key and aggregate data models, making it suitable for a variety of analytical workloads including real-time dashboards, ad-hoc queries, and complex analytical tasks.

Rill supports connecting to an existing StarRocks cluster via a "live connector" and using it as an OLAP engine built against [external tables](/build/connectors/olap#external-olap-tables) to power Rill dashboards.
:::note Supported Versions

Rill supports connecting to StarRocks 4.0 or newer versions.

:::

:::info

Rill supports connecting to an existing StarRocks cluster via a read-only OLAP connector and using it to power Rill dashboards with [external tables](/build/connectors/olap#external-olap-tables).

:::

## Connect to StarRocks

When using StarRocks for local development, you can connect via connection parameters or by using the DSN.
When using StarRocks for local development, you can connect via connection parameters or by using a DSN.

After selecting "Add Data", select StarRocks and fill in your connection parameters. This will automatically create the `starrocks.yaml` file in your `connectors` directory and populate the `.env` file with `connector.starrocks.password`.

Expand All @@ -34,7 +42,7 @@ ssl: false

### Connection String (DSN)

Rill can also connect to StarRocks using a DSN connection string. StarRocks uses MySQL protocol, so the connection string follows the MySQL DSN format:
Rill can also connect to StarRocks using a DSN connection string. StarRocks uses MySQL protocol, so the connection string must follow the MySQL DSN format:

```yaml
type: connector
Expand All @@ -43,28 +51,46 @@ driver: starrocks
dsn: "{{ .env.connector.starrocks.dsn }}"
```

The DSN format is:
#### Using default_catalog

For `default_catalog`, you can specify database directly in the DSN path (MySQL-style):
```
starrocks://user:password@host:port/database
user:password@tcp(host:9030)/my_database?parseTime=true
```

Or using MySQL-style format:
```
user:password@tcp(host:port)/database?parseTime=true
#### Using external catalogs with DSN

For external catalogs (Iceberg, Hive, etc.), set `catalog` and `database` as separate properties (do not include database in DSN):
```yaml
type: connector
driver: starrocks

dsn: "user:password@tcp(host:9030)/?parseTime=true"
catalog: iceberg_catalog
database: my_database
```

If `catalog` is not specified, it defaults to `default_catalog`.

:::warning DSN Format

Only MySQL-style DSN format is supported. The `starrocks://` URL scheme is **not** supported. When using DSN, do not set `host`, `port`, `username`, `password` separately — these must be included in the DSN string.

:::

## Configuration Properties

| Property | Description | Default |
|----------|-------------|---------|
| `host` | StarRocks FE (Frontend) server hostname | Required |
| `host` | StarRocks FE (Frontend) server hostname | Required (if no DSN) |
| `port` | MySQL protocol port of StarRocks FE | `9030` |
| `username` | Username for authentication | Required |
| `username` | Username for authentication | `root` |
| `password` | Password for authentication | - |
| `catalog` | StarRocks catalog name (for external catalogs like Iceberg, Hive) | `default_catalog` |
| `database` | StarRocks database name | - |
| `ssl` | Enable SSL/TLS encryption | `false` |
| `dsn` | Full connection string (alternative to individual parameters) | - |
| `dsn` | MySQL-format connection string (alternative to individual parameters) | - |
| `log_queries` | Enable logging of all SQL queries (useful for debugging) | `false` |

## External Catalogs

Expand Down Expand Up @@ -95,6 +121,26 @@ StarRocks uses a three-level hierarchy: Catalog > Database > Table. In Rill's AP
| `databaseSchema` | Database | `my_database` |
| `table` | Table | `my_table` |

## Creating Metrics Views

When creating metrics views against StarRocks tables, use the `table` property with `database_schema` to reference your data:

```yaml
type: metrics_view
display_name: My Dashboard
table: my_table
database_schema: my_database
timeseries: timestamp

dimensions:
- name: category
column: category

measures:
- name: total_count
expression: COUNT(*)
```

## Troubleshooting

### Connection Issues
Expand All @@ -106,10 +152,14 @@ If you encounter connection issues:
3. Ensure network connectivity to the StarRocks FE node
4. For SSL connections, verify SSL is enabled on the StarRocks server

### Timezone Handling

All timestamp values are returned in UTC. The driver parses DATETIME values from StarRocks as UTC time.

## Known Limitations

- **Model execution**: Model creation and execution is not yet supported. This feature is under development.
- **Read-only connector**: StarRocks is a read-only OLAP connector. Model creation and execution is not supported.
- **Direct table reference**: Use the `table` property in metrics views instead of `model` to reference StarRocks tables directly.

:::info Need help connecting to StarRocks?

Expand Down
11 changes: 7 additions & 4 deletions runtime/drivers/olap.go
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ func (d Dialect) OrderByExpression(name string, desc bool) string {
if desc {
res += " DESC"
}
if d == DialectDuckDB {
if d == DialectDuckDB || d == DialectStarRocks {
res += " NULLS LAST"
}
return res
Expand Down Expand Up @@ -619,9 +619,12 @@ func (d Dialect) DateTruncExpr(dim *runtimev1.MetricsViewSpec_Dimension, grain r
}
return fmt.Sprintf("CAST(date_trunc('%s', %s, 'MILLISECONDS', '%s') AS TIMESTAMP)", specifier, expr, tz), nil
case DialectStarRocks:
// StarRocks supports date_trunc similar to DuckDB but does not support timezone parameter
// NOTE: Timezone and time shift parameters are validated in runtime/metricsview/executor/executor_validate.go
return fmt.Sprintf("date_trunc('%s', %s)", specifier, expr), nil
// StarRocks supports date_trunc and CONVERT_TZ for timezone handling
if tz == "" {
return fmt.Sprintf("date_trunc('%s', %s)", specifier, expr), nil
}
// Convert to target timezone, truncate, then convert back to UTC
return fmt.Sprintf("CONVERT_TZ(date_trunc('%s', CONVERT_TZ(%s, 'UTC', '%s')), '%s', 'UTC')", specifier, expr, tz, tz), nil
default:
return "", fmt.Errorf("unsupported dialect %q", d)
}
Expand Down
139 changes: 134 additions & 5 deletions runtime/drivers/starrocks/olap.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package starrocks

import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
Expand Down Expand Up @@ -86,8 +87,20 @@ func (c *connection) Query(ctx context.Context, stmt *drivers.Statement) (*drive
return nil, err
}

cts, err := rows.ColumnTypes()
if err != nil {
rows.Close()
return nil, err
}

starrocksRows := &starrocksRows{
Rows: rows,
scanDest: prepareScanDest(schema),
colTypes: cts,
}

return &drivers.Result{
Rows: rows,
Rows: starrocksRows,
Schema: schema,
}, nil
}
Expand Down Expand Up @@ -150,8 +163,10 @@ func (c *connection) databaseTypeToRuntimeType(dbType string) (*runtimev1.Type,
}

switch dbType {
case "BOOLEAN", "BOOL", "TINYINT":
case "BOOLEAN", "BOOL":
return &runtimev1.Type{Code: runtimev1.Type_CODE_BOOL}, nil
case "TINYINT":
return &runtimev1.Type{Code: runtimev1.Type_CODE_INT8}, nil
case "SMALLINT":
return &runtimev1.Type{Code: runtimev1.Type_CODE_INT16}, nil
case "INT", "INTEGER":
Expand All @@ -164,8 +179,8 @@ func (c *connection) databaseTypeToRuntimeType(dbType string) (*runtimev1.Type,
return &runtimev1.Type{Code: runtimev1.Type_CODE_FLOAT32}, nil
case "DOUBLE":
return &runtimev1.Type{Code: runtimev1.Type_CODE_FLOAT64}, nil
case "DECIMAL", "DECIMALV2", "DECIMAL32", "DECIMAL64", "DECIMAL128":
return &runtimev1.Type{Code: runtimev1.Type_CODE_DECIMAL}, nil
case "DECIMAL":
return &runtimev1.Type{Code: runtimev1.Type_CODE_STRING}, nil
case "CHAR", "VARCHAR", "STRING", "TEXT":
return &runtimev1.Type{Code: runtimev1.Type_CODE_STRING}, nil
case "DATE":
Expand All @@ -182,8 +197,122 @@ func (c *connection) databaseTypeToRuntimeType(dbType string) (*runtimev1.Type,
return &runtimev1.Type{Code: runtimev1.Type_CODE_STRUCT}, nil
case "BINARY", "VARBINARY", "BLOB":
// Note: StarRocks doesn't have BLOB type, but MySQL driver may report VARBINARY as BLOB
return &runtimev1.Type{Code: runtimev1.Type_CODE_BYTES}, nil
// Use CODE_STRING like MySQL driver for consistency
return &runtimev1.Type{Code: runtimev1.Type_CODE_STRING}, nil
default:
return nil, errUnsupportedType
}
}

// starrocksRows wraps sqlx.Rows to provide MapScan method.
// This is required because if the correct type is not provided to Scan
// mysql driver just returns byte arrays.
type starrocksRows struct {
*sqlx.Rows
scanDest []any
colTypes []*sql.ColumnType
}

func (r *starrocksRows) MapScan(dest map[string]any) error {
err := r.Rows.Scan(r.scanDest...)
if err != nil {
return err
}
for i, ct := range r.colTypes {
fieldName := ct.Name()
valPtr := r.scanDest[i]
// Safety guard: prepareScanDest always allocates, but check anyway
if valPtr == nil {
dest[fieldName] = nil
continue
}
switch valPtr := valPtr.(type) {
case *sql.NullBool:
if valPtr.Valid {
dest[fieldName] = valPtr.Bool
} else {
dest[fieldName] = nil
}
case *sql.NullInt16:
if valPtr.Valid {
dest[fieldName] = valPtr.Int16
} else {
dest[fieldName] = nil
}
case *sql.NullInt32:
if valPtr.Valid {
dest[fieldName] = valPtr.Int32
} else {
dest[fieldName] = nil
}
case *sql.NullInt64:
if valPtr.Valid {
dest[fieldName] = valPtr.Int64
} else {
dest[fieldName] = nil
}
case *sql.NullFloat64:
if valPtr.Valid {
dest[fieldName] = valPtr.Float64
} else {
dest[fieldName] = nil
}
case *sql.NullString:
if valPtr.Valid {
dest[fieldName] = valPtr.String
} else {
dest[fieldName] = nil
}
case *sql.NullTime:
if valPtr.Valid {
dest[fieldName] = valPtr.Time
} else {
dest[fieldName] = nil
}
default:
// Handle ARRAY, MAP, STRUCT, BYTES and other complex types
// These are scanned into *any in prepareScanDest
if ptr, ok := valPtr.(*any); ok {
dest[fieldName] = *ptr
} else {
// Fallback: store the pointer's underlying value directly
dest[fieldName] = valPtr
}
}
}
return nil
}

func prepareScanDest(schema *runtimev1.StructType) []any {
scanList := make([]any, len(schema.Fields))
for i, field := range schema.Fields {
var dest any
switch field.Type.Code {
case runtimev1.Type_CODE_BOOL:
dest = &sql.NullBool{}
case runtimev1.Type_CODE_INT8:
dest = &sql.NullInt16{}
case runtimev1.Type_CODE_INT16:
dest = &sql.NullInt16{}
case runtimev1.Type_CODE_INT32:
dest = &sql.NullInt32{}
case runtimev1.Type_CODE_INT64, runtimev1.Type_CODE_INT128:
dest = &sql.NullInt64{}
case runtimev1.Type_CODE_FLOAT32, runtimev1.Type_CODE_FLOAT64:
dest = &sql.NullFloat64{}
case runtimev1.Type_CODE_STRING:
dest = &sql.NullString{}
case runtimev1.Type_CODE_DATE, runtimev1.Type_CODE_TIME:
dest = &sql.NullString{}
case runtimev1.Type_CODE_TIMESTAMP:
// MySQL driver returns DATETIME as time.Time when parseTime=true in DSN
dest = &sql.NullTime{}
case runtimev1.Type_CODE_JSON:
dest = &sql.NullString{}
default:
dest = new(any)
}
scanList[i] = dest
}
return scanList
}
Loading
Loading