| name | fiftyone-generate-data-lens-connector |
|---|---|
| description | Generate a Data Lens connector from an external database schema. Use when users want to connect an external data source (PostgreSQL, BigQuery, Databricks, MySQL, etc.) to FiftyOne Data Lens, or when they have a database schema and want to browse/import that data through the FiftyOne App. |
Generate a fully functional DataLensOperator plugin from a user-provided
database
schema. The generated connector lets users browse, preview, and import samples
from
their external data source directly through the FiftyOne App's Data Lens panel.
Data Lens is a FiftyOne Enterprise feature. Before proceeding, inform the user:
Note: Data Lens is an enterprise-only feature available in FiftyOne Enterprise. The connector generated by this skill requires a FiftyOne Enterprise deployment to run. If you're using the open-source version of FiftyOne, this connector will not work in your environment.
Would you like to proceed?
Wait for the user to confirm before continuing. If they ask about alternatives for OSS, suggest they look into custom operators for similar data-import workflows, or the standard dataset import utilities.
ALWAYS follow these rules:
Never generate connector code until you fully understand the source schema. Ask clarifying questions about anything ambiguous — column semantics, coordinate systems, filepath conventions, image dimensions.
Present a clear table mapping source columns to FiftyOne field types. Get user approval before writing any code. This is the most important step — a connector that maps fields wrong is worse than no connector.
Never interpolate user input directly into SQL strings. Use the database
driver's
parameterized query mechanism (e.g., %s for psycopg, @param for BigQuery,
:param for Databricks).
Always yield DataLensSearchResponse objects in batches of
request.batch_size.
Buffer results and yield when the buffer is full, plus a final yield for
remaining
samples.
Generate only what's needed. Don't add vector search, text search, or advanced features unless the user's schema and requirements call for them. A simple metadata filter connector is the right default.
Gather the information needed to generate the connector:
-
Get the schema. Accept any of these formats:
- DDL (
CREATE TABLEstatements) - Column list with types (JSON, markdown table, plain text)
- A request to introspect a live database (guide the user to export the schema)
- DDL (
-
Identify key columns. Ask about any that aren't obvious:
- Filepath column — which column contains the media path? Is it absolute, relative (needs a prefix), or a cloud URI?
- Label columns — which columns contain annotations? What format? (JSON blobs, foreign key joins, flat columns)
- Metadata columns — which columns should become filterable properties?
- Coordinate system — if bounding boxes exist: pixel-absolute or normalized? What are the image dimensions (fixed or per-sample)?
-
Identify the database type. This determines:
- Which Python driver to use
- Query syntax (parameterization style, JSON functions, etc.)
- Connection string format and required secrets
-
Get sample data (if available). Even 2-3 example rows dramatically improve the quality of the field mapping. Ask for them.
Present a mapping table for user approval:
| Source Column | FiftyOne Field | Type | Notes |
|-------------------|-----------------------|---------------------|--------------------------------|
| filepath | filepath | str | Prefix: gs://bucket/path/ |
| weather | weather | Classification | Filterable enum |
| bbox_x1/y1/x2/y2 | detections | Detections | Pixel coords, normalize by WxH |
| category | detections[].label | str | Detection label |
| ... | ... | ... | ... |
Include:
- Filepath construction — how the full path is built from the column value
- Coordinate normalization — formula if bounding boxes are in pixel coordinates
- Label hierarchy — how nested/joined label data maps to FiftyOne label types
- Filters — which columns become
resolve_inputenum/text fields, with known values if available
Wait for user approval before proceeding.
Generate a complete plugin directory with these files:
| File | Purpose |
|---|---|
__init__.py |
Operator class + handler class + sample transform |
fiftyone.yml |
Plugin manifest with operator name and secrets |
requirements.txt |
Python driver dependency |
Use the patterns from CONNECTOR-TEMPLATE.md as your structural guide. The generated code should follow these architectural layers:
Layer 1 — Operator class (DataLensOperator subclass):
configproperty withexecute_as_generator=True,unlisted=Trueresolve_input()defining the UI filter formhandle_lens_search_request()delegating to the handler
Layer 2 — Handler class (connection + query logic):
- Context manager for connection lifecycle
iter_batches()implementing the batching loop_generate_query()building parameterized SQL fromsearch_params_transform_sample()mapping raw rows tofo.Sample(...).to_dict()
Layer 3 — Data models (optional, for complex schemas):
@dataclassfor query parameters (validated fromsearch_params)@dataclassfor query result rows (typed column access)
See FIELD-MAPPING-GUIDE.md for the rules on mapping database types to FiftyOne field types, especially for spatial data (bounding boxes, keypoints, segmentation masks).
After generating the connector:
-
Syntax check — the generated code must parse without errors:
python -c "import ast; ast.parse(open('__init__.py').read()); print('OK')" -
Sample construction check — if sample data was provided, construct
fo.Sampleobjects from it and verify they serialize correctly:import fiftyone as fo sample = fo.Sample( filepath="...", # ... mapped fields ) sample.to_dict() # Must not raise
-
Walk through the generated code with the user:
- Confirm the query logic matches their schema
- Confirm the transform logic produces correct FiftyOne samples
- Confirm the
resolve_inputfilters are right
-
Installation guidance:
# Copy plugin to FiftyOne plugins directory PLUGINS_DIR=$(python -c "import fiftyone as fo; print(fo.config.plugins_dir)") cp -r ./my-connector "$PLUGINS_DIR/" # Set required secrets export MY_SECRET_KEY="..." # Register in Data Lens UI: Data sources > Add > plugin-name/operator_name
The first generation likely needs refinement. Common adjustments:
- Adding/removing filter fields
- Fixing coordinate normalization
- Adjusting the filepath prefix
- Handling NULL/missing values in optional columns
- Adding conditional WHERE clauses for "all" filter values
# Driver: psycopg[binary]
# Parameterization: %s positional
# Connection: ctx.secret("POSTGRES_CONNECTION_STRING")
# JSON: JSON_AGG, JSON_BUILD_OBJECT
# Streaming: cursor.stream(query, params, size=batch_size)
import psycopg
from psycopg.rows import dict_row# Driver: google-cloud-bigquery
# Parameterization: @param with QueryJobConfig
# Connection: project from ctx.secret("BIGQUERY_PROJECT")
# JSON: JSON_VALUE, JSON_QUERY
from google.cloud import bigquery
job_config = bigquery.QueryJobConfig(
query_parameters=[
bigquery.ScalarQueryParameter("name", "STRING", value)
]
)# Driver: databricks-sdk
# Parameterization: :param with StatementParameterListItem
# Connection: ctx.secret("DATABRICKS_HOST"), ctx.secret("DATABRICKS_HTTP_PATH")
# JSON: Standard SQL
from databricks.sdk import WorkspaceClient
from databricks.sdk.service.sql import StatementParameterListItem# Driver: mysql-connector-python
# Parameterization: %s positional (same as psycopg)
# Connection: ctx.secret("MYSQL_CONNECTION_STRING")
import mysql.connector# Driver: built-in sqlite3
# Parameterization: ? positional
# Connection: file path from ctx.secret("SQLITE_DB_PATH") or ctx.params
import sqlite3foo.OperatorConfig(
name="my_connector",
label="My Connector",
execute_as_generator=True, # REQUIRED for Data Lens
unlisted=True, # Hide from operator browser
)buffer = []
for row in query_results:
buffer.append(transform(row))
if len(buffer) >= request.batch_size:
yield DataLensSearchResponse(
result_count=len(buffer),
query_result=buffer,
)
buffer = []
if buffer:
yield DataLensSearchResponse(
result_count=len(buffer),
query_result=buffer,
)# Pattern: skip filter when user selects "all"
WHERE
1 = 1
AND(1 = {1 if param == "all" else 0}
OR
column = %s)import fiftyone as fo
import fiftyone.operators as foo
import fiftyone.operators.types as types
from fiftyone.operators.data_lens import (
DataLensOperator,
DataLensSearchRequest,
DataLensSearchResponse,
)