
Initial Release
Dataset Explorer and Import Studio for InterSystems IRIS. Find a dataset on the Hugging Face Hub, look inside it, and load it into an IRIS table: from a web page, with one line of ObjectScript, or with one SQL statement.
Discover
Import
Data quality
float32 values keep their digits, decimals, dates and timestamps keep their types.VARCHAR columns, so =, LIKE, GROUP BY and DISTINCT work as usual.<column>_name column.Manage
Operate
git clone https://github.com/mwaseem75/iris-hf-loader.git
cd iris-hf-loader
docker compose up -d --build --wait
Open http://localhost:52773/hf2iris/index.html and sign in with _SYSTEM / SYS.
The tutorial takes about 15 minutes. You need Docker Desktop (or Docker with Compose v2), git, internet access for huggingface.co, and about 5 GB of free disk space.
git clone https://github.com/mwaseem75/iris-hf-loader.git
cd iris-hf-loader
docker compose up -d --build --wait
The first build takes a few minutes. It downloads the IRIS image and installs the Python libraries for embedded Python (datasets, pandas, pyarrow). --wait returns when IRIS is up and the package is installed in the USER namespace.
The container listens on your own machine only:
| Address | |
|---|---|
| Web app | http://localhost:52773/hf2iris/index.html |
| Management Portal | http://localhost:52773/csp/sys/UtilHome.csp |
| SQL (DBeaver, JDBC, ODBC) | localhost:1972, namespace USER |
The login is _SYSTEM / SYS. This is a demo setup; do not expose it to a network. If port 52773 or 1972 is taken, choose other ones:
IRIS_WEB_PORT=8080 IRIS_SQL_PORT=51972 docker compose up -d --wait
Open http://localhost:52773/hf2iris/index.html and sign in. The address needs the index.html at the end.
The Explore page starts with the most downloaded datasets on the Hub. Type imdb in the search box. You can sort the results by Downloads, Likes, Trending or Recently updated, and narrow them down by Task, Size and Language. The search and filters are part of the page address, so you can bookmark or share a search.
Each card shows the dataset id, a short description, its task, size and license, the number of downloads and likes, and when it was last updated. A yellow Gated badge means the dataset needs a token (see step 12).
Click stanfordnlp/imdb. The dataset page shows the description, badges, downloads and likes, and a link to the dataset on Hugging Face.
Pick a Config and a Split; each split shows its row count and size. Then use the three tabs:
[image] or [audio]. Nothing is imported or downloaded into IRIS for this.The tabs work with the keyboard too: the arrow keys, Home and End move between them.
If the dataset viewer cannot show a dataset, the Preview tab offers Read the first rows directly. That reads the dataset files themselves and can take up to a minute, so it only happens when you click it.
The Import into IRIS panel sits next to the tabs. For the IMDB example:
test split.Demo.IMDB. The default is HF.<dataset name>.1000. 0 imports all rows.Click Review plan. Nothing is written yet. The plan says whether the table will be created, replaced or appended to, how many rows to expect, which commit will be read and how much has to be downloaded. If the import would not be allowed, for example because the target table exists and was not created by this tool, the plan says why and Start import stays disabled.
Click Start import. The import runs in the background inside IRIS, and the panel follows it: “Looking up the dataset”, “Downloading rows from Hugging Face”, then “Writing rows to IRIS” with a progress bar. Cancel import stops it before the next batch of rows; a cancelled import leaves nothing behind. When it is done, the panel links to the new table.
Under the panel, Same import in ObjectScript or SQL shows the one-line command for exactly this import, with copy buttons.
One thing worth knowing about IMDB: the train split is sorted by label, so its first 1000 rows are all negative reviews. That is why this example uses the test split.
Open Imports in the side menu.
Open Tables in the side menu. It lists every table the loader created, with its rows, columns, source dataset, commit and last import. Filter the list by name.
Click a table to open it:
Copy SELECT copies a ready-made query. Re-import opens the dataset page with the same config, split and table filled in, which is the quickest way to refresh a table.
The imported table is an ordinary IRIS table, so every SQL tool can use it.
Demo and click Demo.IMDB to see its columns.SELECT TOP 5 label_, label_name, text_ FROM Demo.IMDB
Notice the column names: text and label became text_ and label_. The loader renames column names that are SQL reserved words, replaces spaces and symbols with _, and removes accents (Café becomes Cafe). label_name was added by the loader; see step 11.
Text columns compare exactly, so 'Iris-setosa' and 'IRIS-SETOSA' are different values. For a case-insensitive search use %SQLUPPER:
SELECT COUNT(*) FROM Demo.IMDB WHERE %SQLUPPER(label_name) = %SQLUPPER('NEG')
docker compose exec iris iris session iris -U USER
USER>do ##class(dc.hf2iris.Loader).Load("stanfordnlp/imdb","Demo.IMDB","test",1000)
Fetching stanfordnlp/imdb [default:test] @ e628166 limit 1000 Created Demo.IMDB (3 columns) 1,000/1,000 rows Loaded 1000 rows into Demo.IMDB in .48s (commit e628166)
With only a dataset id, the whole train split goes into HF.<name>:
do ##class(dc.hf2iris.Loader).Load("scikit-learn/iris")
The arguments of Load:
| Argument | Default | Meaning |
|---|---|---|
| dataset | Hugging Face id, for example stanfordnlp/imdb |
|
| table | HF.<name> |
target table, Schema.Table |
| split | train |
which split to load |
| limit | 0 |
maximum number of rows; 0 means all |
| config | "" |
config (subset), needed when a dataset has several |
| mode | replace |
replace, append, or fail when the table exists |
| revision | "" |
branch, tag or commit hash; empty means the latest version |
| result | output: the result as a %DynamicObject |
Load returns a %Status. To see which configs and splits a dataset has before loading it:
write ##class(dc.hf2iris.Loader).Info("openai/gsm8k").%ToJSON()
{"dataset":"openai/gsm8k","configs":{"main":["train","test"],"socratic":["train","test"]}}
The loader is also available as two SQL functions, so any SQL client can import: the Management Portal, DBeaver, JDBC, ODBC or Python DB-API. The client needs neither Python nor a Hugging Face library.
SELECT dc_hf2iris.load_dataset('scikit-learn/iris', 'HF.iris')
SELECT dc_hf2iris.dataset_info('openai/gsm8k')
The arguments are the same as in the table above, in the same order; leave out trailing ones or pass NULL. load_dataset returns the result as JSON with "status":"OK". A failed import does not raise an SQL error; it returns {"status":"ERROR","error":"..."}, which is easier to handle in scripts.
Some datasets have several configs. openai/gsm8k (grade school math questions) has main and socratic, and if you leave out the config you are told which ones exist:
SELECT dc_hf2iris.load_dataset('openai/gsm8k', 'Demo.GSM8K', 'test', 200) -- {"status":"ERROR","error":"This dataset has several configs, choose one: main, socratic"}
SELECT dc_hf2iris.load_dataset('openai/gsm8k', 'Demo.GSM8K', 'test', 200, 'main')
A wrong split name works the same way: the error lists the splits that exist. These checks happen before anything is downloaded.
Datasets change over time. When you need the same rows every time, for a demo, a test or a report, pass a commit hash as the revision:
SELECT dc_hf2iris.load_dataset('rotten_tomatoes', 'Demo.Reviews', 'test', 0, NULL, NULL,
'aa13bc287fa6fcab6daf52f0dfb9994269ffea28')
The result then says "pinned": true. Every import, pinned or not, records the commit it read; you see it in the result, on the Imports and Tables pages and in dc_hf2iris.ImportLog. A branch or tag name works too and is turned into a commit once, at the start of the import.
rotten_tomatoes is an old style id without an owner. Recent versions of the Hugging Face library reject those, so the loader looks up the full id (cornell-movie-review-data/rotten_tomatoes) first.
Classification datasets store their labels as numbers: IMDB uses 0 and 1, and you would have to look up what they mean. The loader keeps the names by adding a <column>_name column right after each label column:
SELECT label_, label_name, COUNT(*) FROM Demo.Reviews GROUP BY label_, label_name
Multi-label datasets work too. In google-research-datasets/go_emotions (config simplified) a row can have several labels, and you get both lists, for example [2] and ["anger"]. The value -1, which some datasets use for “no label”, becomes NULL. The full list of label names is in the import result (classLabels) and in ImportLog.ClassLabels.
Some datasets are gated: you accept their terms on huggingface.co before you can download them. For those, and for your own private datasets, create an access token in your Hugging Face settings and start the container with it:
HF_TOKEN=hf_xxx docker compose up -d --wait
In PowerShell:
$env:HF_TOKEN = "hf_xxx"
docker compose up -d --wait
The status badge in the top bar then says HF token set. Without a token, a gated dataset’s page explains what to do instead of showing an error.
The token is only passed into the container when it is set. All Hugging Face requests go out from IRIS, so the token never reaches the browser, and it is replaced by *** if it ever appears in an error message or the import log.
The web app is built on a REST API that you can use directly. It uses basic authentication. https://github.com/mwaseem75/iris-hf-loader/blob/main/API.md describes every endpoint with its parameters, responses and error codes.
# search and look inside datasets curl -u _SYSTEM:SYS "http://localhost:52773/api/hf2iris/datasets?search=squad&sort=likes" curl -u _SYSTEM:SYS "http://localhost:52773/api/hf2iris/dataset?id=openai/gsm8k" curl -u _SYSTEM:SYS "http://localhost:52773/api/hf2iris/dataset/preview?id=openai/gsm8k&config=main&split=test&rows=5"plan and start a background import, then follow it
curl -u _SYSTEM:SYS -X POST -H "Content-Type: application/json"
-d '{"dataset":"openai/gsm8k","config":"main","split":"test","table":"Demo.GSM8K"}'
http://localhost:52773/api/hf2iris/plan curl -u _SYSTEM:SYS -X POST -H "Content-Type: application/json"
-d '{"dataset":"openai/gsm8k","config":"main","split":"test","table":"Demo.GSM8K"}'
http://localhost:52773/api/hf2iris/jobs curl -u _SYSTEM:SYS http://localhost:52773/api/hf2iris/jobs/1history and tables
curl -u _SYSTEM:SYS http://localhost:52773/api/hf2iris/history curl -u _SYSTEM:SYS http://localhost:52773/api/hf2iris/tables curl -u _SYSTEM:SYS "http://localhost:52773/api/hf2iris/preview/Demo.GSM8K?rows=5&offset=0"
POST /load also imports, but it answers only when the import is finished. Use /jobs for anything that takes longer than a few seconds: the web gateway in front of IRIS gives up on a request after 60 seconds, while a background job keeps running.
Inside IRIS embedded Python (a Language = python method, or irispython) the package can be used directly:
import hf2iris
r = hf2iris.load("stanfordnlp/imdb", "Demo.IMDB", split="test", limit=1000)
print(r["rows"], r["columns"])
To load a pandas DataFrame or a pyarrow Table you already have:
from hf2iris.loader import IrisSQL, load_dataframe
load_dataframe(df, "Demo.Mine", IrisSQL())
The button in the top bar switches between the dark and the light theme; the choice is remembered. Both themes meet the WCAG contrast guidelines.
A dataset page in the light theme.
On your own IRIS (2023.1 or newer, with embedded Python), once the package is published on Open Exchange:
zpm "install iris-hf-loader"
Until then, or to install your own changes, load it from a clone of this repository:
zpm "load /path/to/iris-hf-loader"
IPM installs the Python libraries from requirements.txt, the classes, the REST application /api/hf2iris and the web app /hf2iris/index.html.
Set these in your shell before docker compose up; they are passed into the container when set.
| Variable | Default | Effect |
|---|---|---|
HF_TOKEN |
Hugging Face token for gated and private datasets | |
HF2IRIS_MAX_ROWS |
1000000 | the largest split a load without a row limit may import |
HF2IRIS_MAX_DOWNLOAD_MB |
500 | the largest download a load without a row limit may start |
HF2IRIS_EXACT |
1 | 0 creates text columns with the IRIS default collation instead of exact |
IRIS_WEB_PORT, IRIS_SQL_PORT |
52773, 1972 | host ports of the container |
These are read by the loader too; to use them with Docker, add them to the environment: list in docker-compose.yml:
| Variable | Default | Effect |
|---|---|---|
HF2IRIS_CACHE_TTL |
600 | seconds Hugging Face answers are cached (search: at most 300); 0 turns the cache off |
HF2IRIS_SHOW_PRIVATE |
0 | 1 lists your token’s private datasets in search results |
HF2IRIS_MAX_JOBS |
2 | how many background imports may run at the same time |
dc.hf2iris.REST). All Hugging Face requests go out from IRIS.dc.hf2iris.Hub, Python hf2iris.hub) uses the Hugging Face Hub API for search and metadata, and the dataset viewer for splits, sizes and preview rows. Answers are cached in IRISTEMP for a few minutes.hf2iris.plan) checks the dataset, config, split and target table, and computes the table definition from the dataset’s schema and a sample of rows, without writing anything.dc.hf2iris.Loader, Python hf2iris.loader) reads the split as an Arrow table, streaming only the requested rows when there is a row limit, maps each column to an IRIS type, creates the table and inserts the rows in batches inside one transaction.dc.hf2iris.Job) run the same import in their own IRIS process and report their progress, so the web request returns at once.dc_hf2iris.ImportLog.Column types:
| Dataset type | IRIS type |
|---|---|
| integers, also with missing values | BIGINT |
| float16, float32, float64 | DOUBLE (a float32 1.4 stays 1.4) |
| bool | BIT |
| decimal | NUMERIC(p,s) |
| date | DATE |
| timestamp | TIMESTAMP (UTC) |
| text | VARCHAR(n) |
| lists, structs, embeddings | VARCHAR(n) holding JSON |
| text longer than 1,000,000 characters | LONGVARCHAR |
| class label | BIGINT, plus a <column>_name column with the label |
n is the length of the longest value, rounded up to a multiple of 1000 and at least 4000.
replace and append only touch tables the loader created. Any other existing table makes the import stop without changing it.replace leaves no table, because the old one was already dropped.Everything at once, against the running container:
tests/run_all.sh # changes no data
tests/run_all.sh --with-imports # also imports real datasets, then removes everything it added
Or one part at a time:
pip install pandas pyarrow pytest pytest tests # Python logic with SQLite and fakes: no IRIS, no network node --test tests/ui/state.test.mjs # UI helpers, contrast of both themes node tests/ui/mocked.mjs # import monitoring in the UI against a fake API
tests/run_iris_tests.sh # the Python tests inside IRIS, plus tests against real IRIS SQL tests/smoke.sh discovery # every REST endpoint against the live Hub, read-only tests/smoke.sh import # real imports and background jobs; drops its tables afterwards node tests/ui/live.mjs # the web pages against the server node tests/ui/browser.mjs # the app in headless Chrome
The browser check starts Chrome headless with an empty, throwaway profile and only opens the local app. It covers sign-in, search, navigation, the theme, the phone layout and console errors; --screenshots saves fresh images to docs/screenshots. You need Node 24, and Chrome for that check. The scripts are bash; on Windows run them from Git Bash. GitHub Actions runs the offline tests on every push.
“Not Found” at http://localhost:52773/hf2iris/. Add index.html: http://localhost:52773/hf2iris/index.html.
The page says “Unauthorized”. Reload with Ctrl+F5 and sign in. If it keeps happening, click Sign out in the top bar and sign in again.
The page looks outdated after an update. IRIS lets the browser keep static files for an hour. Reload with Ctrl+F5.
I changed the code and nothing happened. In the USER namespace run zpm "load /home/irisowner/dev/ -v -bypass-py-deps", then docker compose restart. The web server processes keep the old Python code in memory until they restart. -bypass-py-deps keeps IPM from reinstalling the Python libraries, which would replace the pinned versions of the image.
An import is slow. Use a row limit first, and use the background import (the web page always does). Downloads are cached in the container under /home/irisowner/.cache/huggingface.
“Hugging Face rate limit reached.” Wait a minute, or start the container with HF_TOKEN; requests with a token get a higher limit.
python/hf2iris/ Python package: loader.py (download, types, insert), hub.py (search,
details, previews), plan.py (import plans)
src/dc/hf2iris/ Loader.cls (ObjectScript and SQL entry points), Hub.cls, Job.cls
(background imports), Tables.cls, ImportLog.cls, REST.cls
web/ the web app: index.html, app.css, js/ (no framework, no build step)
tests/ Python tests, UI tests (tests/ui), run_all.sh and the other scripts
docs/screenshots/ images for this README
module.xml IPM package
Dockerfile, docker-compose.yml, iris.script
requirements.txt Python libraries for IPM installs
constraints.txt exact versions used in the Docker image
https://github.com/mwaseem75/iris-hf-loader/blob/main/API.md REST API reference
The Docker image is pinned to IRIS 2026.1.0.234 and to the package versions in constraints.txt, the versions everything was tested with.
MIT. See https://github.com/mwaseem75/iris-hf-loader/blob/main/LICENSE.