Run your PySpark DataFrame code on Hardcharger™, with no Spark cluster.
⚡ What is it?
The Hardcharger™ backend for SQLFrame, which implements the PySpark DataFrame API on SQL engines. Hardcharger is GizmoData™'s DuckDB 2.0 server; this package turns PySpark-style transformations into DuckDB SQL and runs them there, so Spark and Databricks jobs move over by swapping the session builder, not by rewriting them.
- The PySpark API:
SparkSession, DataFrames,functions,Window, the catalog, andactivate()so that unchangedfrom pyspark.sql import ...code runs on Hardcharger. - Bulk loading:
session.ingest()streams Arrow tables, record batch readers and pandas DataFrames in through ADBC bulk ingest, as one transaction. - Client-local files:
spark.read.json()/.csv()/.parquet()on files on your machine parse them client-side (Spark-style schema inference for JSON) and bulk-load them into temporary tables; the server needs no access to your file system. - Connections:
adbc-driver-hardcharger, withhardcharger://URIs (TLS on by default, port 31337), password, access token, client certificate (mutual TLS) and browser (OAuth/SSO) sign-in, and OpenTelemetry traces of every statement.
📦 Install
pip install sqlframe-hardcharger
Python 3.11 or later. It installs SQLFrame, pyarrow and the driver,
adbc-driver-hardcharger, from PyPI. Both ship with Hardcharger 2.0.0.
🚀 Quick start
from sqlframe_hardcharger import HardchargerSession
spark = (
HardchargerSession.builder.config("hardcharger.uri", "hardcharger://localhost")
.config("hardcharger.username", "hardcharger")
.config("hardcharger.password", "change-me")
.getOrCreate()
)
people = spark.createDataFrame(
data=[(1, "Alice", 34), (2, "Bob", 27), (3, "Carol", 41)],
schema=["id", "name", "age"],
)
people.filter(people.age > 30).select("name", "age").orderBy("age").show()
+-------+-----+
| name | age |
+-------+-----+
| Alice | 34 |
| Carol | 41 |
+-------+-----+
hardcharger://host[:port] is HTTPS on port 31337 unless the URI names another; the server's
certificate is verified. For a development server on plain HTTP, add ?tls=false.
🐍 PySpark code, unchanged
activate() points the pyspark imports at Hardcharger, so existing PySpark code runs as is:
from sqlframe_hardcharger import activate
activate(uri="hardcharger://localhost", username="hardcharger", password="change-me")
from pyspark.sql import SparkSession
from pyspark.sql import functions as F
from pyspark.sql.window import Window
spark = SparkSession.builder.getOrCreate()
sales = spark.createDataFrame(
data=[("east", "2026-10-01", 120.0), ("east", "2026-10-02", 80.0), ("west", "2026-10-01", 200.0)],
schema=["region", "day", "amount"],
)
by_region = sales.groupBy("region").agg(F.sum("amount").alias("total"), F.count("*").alias("days"))
by_region.orderBy("region").show()
running = Window.partitionBy("region").orderBy("day")
sales.withColumn("running_total", F.sum("amount").over(running)).orderBy("region", "day").show()
SparkSession here is a HardchargerSession, so everything below (spark.ingest(),
the client-local readers) works through it too.
🔐 Connecting
Every setting is a hardcharger.<name> config key on the builder, or the same name as a keyword
argument of activate(). They are the keyword arguments of
adbc_driver_hardcharger.dbapi.connect():
| Key | Meaning |
|---|---|
hardcharger.uri |
hardcharger://host[:port] (HTTPS, port 31337); ?tls=false for plain HTTP |
hardcharger.username, hardcharger.password |
Password sign-in |
hardcharger.access_token |
An access token issued elsewhere, used as is |
hardcharger.auth_type |
password (default), client_certificate (mutual TLS, no password), or external (the server's browser sign-in, OAuth/SSO) |
hardcharger.tls_root_certs |
CA certificates to trust (PEM text or a file path) instead of the system's |
hardcharger.tls_skip_verify |
Don't verify the server's certificate (development only) |
hardcharger.tls_client_cert, hardcharger.tls_client_key |
Client certificate and key, for mutual TLS |
hardcharger.oauth_timeout, hardcharger.open_browser |
Browser sign-in: how long to wait, how to open the browser |
hardcharger.session_tag, hardcharger.query_tag |
JSON objects the server records with the session and its statements |
hardcharger.settings |
hardcharger.* session settings, e.g. {"query_timeout": 60} |
hardcharger.catalog, hardcharger.db_schema |
The session's current catalog and schema |
hardcharger.db_kwargs, hardcharger.conn_kwargs |
Any other ADBC option (adbc.hardcharger.*, adbc.warp.*, adbc.telemetry.*) |
A token from your identity platform:
import os
from sqlframe_hardcharger import HardchargerSession
spark = (
HardchargerSession.builder.config("hardcharger.uri", "hardcharger://localhost")
.config("hardcharger.access_token", os.environ["HARDCHARGER_TOKEN"])
.getOrCreate()
)
spark.sql("SELECT HARDCHARGER_USER() AS signed_in_as").show()
A client certificate, on a server started with --mtls-ca-cert and --mtls-certificate-sign-in
(the token the server issues is bound to the certificate):
from sqlframe_hardcharger import HardchargerSession
spark = (
HardchargerSession.builder.config("hardcharger.uri", "hardcharger://localhost")
.config("hardcharger.auth_type", "client_certificate")
.config("hardcharger.tls_client_cert", "client.pem")
.config("hardcharger.tls_client_key", "client-key.pem")
.getOrCreate()
)
spark.sql("SELECT HARDCHARGER_USER() AS signed_in_as").show()
Browser sign-in (hardcharger.auth_type = external) opens the server's sign-in page (a server
started with --oauth-client-id; the sign-in runs on the server's own port) and waits for you to
finish there.
A session is a process-wide singleton, as in SQLFrame: once one exists, getOrCreate() returns it
until you call spark.stop().
📥 Bulk loading with ingest()
createDataFrame() sends rows as a SQL VALUES list, which is fine for a few thousand rows.
For anything bigger, ingest() streams Arrow data in through ADBC bulk ingest and returns a
DataFrame over the table:
import pyarrow as pa
from sqlframe_hardcharger import HardchargerSession
spark = (
HardchargerSession.builder.config("hardcharger.uri", "hardcharger://localhost")
.config("hardcharger.username", "hardcharger")
.config("hardcharger.password", "change-me")
.getOrCreate()
)
events = pa.table(data={"event_id": list(range(1_000_000)), "kind": ["click", "view"] * 500_000})
df = spark.ingest(table_name="events", data=events, mode="replace")
df.groupBy("kind").count().orderBy("kind").show()
data: apyarrow.Table,RecordBatchorRecordBatchReader, or apandas.DataFrame.mode:create(the default; fails if the table exists),append,create_appendorreplace.temporary=Truemakes a temporary table, visible to this session only.- An ingest is one transaction: if it fails part-way, none of its rows remain.
- The driver sends an upload of several batches over up to 8 connections at once, so its rows
land in no particular order;
ingest_parts=1sends one ordered stream.
📂 Reading files on your machine
spark.read.json(), .csv(), .parquet() and .load() notice when the path (a file, a glob
or a directory) exists on the client, parse it there, and bulk-load it into a temporary
table. The server never needs to see your files:
from sqlframe_hardcharger import HardchargerSession
spark = (
HardchargerSession.builder.config("hardcharger.uri", "hardcharger://localhost")
.config("hardcharger.username", "hardcharger")
.config("hardcharger.password", "change-me")
.getOrCreate()
)
with open("orders.jsonl", "w") as f:
f.write('{"id": 1, "customer": {"name": "Alice", "tier": "gold"}, "items": [{"sku": "A1", "qty": 2}]}\n')
f.write('{"id": 2, "customer": {"name": "Bob"}, "items": [], "note": "gift"}\n')
orders = spark.read.json("orders.jsonl")
orders.printSchema()
orders.select("id", orders["customer"]["name"].alias("customer")).orderBy("id").show()
with open("people.csv", "w") as f:
f.write("id,name\n1,Alice\n2,Bob\n")
spark.read.csv("people.csv", header=True, inferSchema=True).show()
- JSON schemas are inferred the way Spark does it: one column per top-level key, objects as
structs, arrays as arrays, fields sorted by name, and a field seen with conflicting types
falling back to
string(its JSON text). Inference streams the files, one record at a time, while they upload. - One deliberate difference from Spark: a nested subtree with more than
maxNestedFieldsfields (default 1000, counted recursively) stays oneJSONcolumn rather than an enormous struct; query it withget_json_object()..option("maxNestedFields", None)expands everything. .option("jsonDocument", True)skips parsing: each document lands as text in a singleJSONcolumn, for data whose shape is too unstable to type.- Also honored:
schema,header,sep/delimiter,multiLine,samplingRatio,filename,encoding. Other options are ignored, with a warning; use.option("serverSideRead", True)to have the server read the path with DuckDB's ownread_json()/read_csv()/read_parquet()(and all their options) instead. - Rows keep the files' order (one ordered upload);
.option("ingestParts", 8)uploads in parallel instead. - A path the client can't see goes to the server as before (
read_csv(...)on the server's file system), as dos3://and other remote URLs.
🔭 OpenTelemetry
The driver traces every statement (Database.Open, Statement.ExecuteQuery,
Statement.ExecuteUpdate) with OpenTelemetry. Turn it on with the standard OTEL_* environment
variables, or per session with the ADBC telemetry options:
from sqlframe_hardcharger import HardchargerSession
spark = (
HardchargerSession.builder.config("hardcharger.uri", "hardcharger://localhost")
.config("hardcharger.username", "hardcharger")
.config("hardcharger.password", "change-me")
.config(
"hardcharger.db_kwargs",
{"adbc.telemetry.traces_exporter": "adbcfile", "adbc.telemetry.traces_folder_path": "traces"},
)
.getOrCreate()
)
spark.sql("SELECT 42 AS answer").show()
adbcfile writes the spans to files in traces/; otlp sends them to an OpenTelemetry
collector (OTEL_EXPORTER_OTLP_ENDPOINT).
🧭 Good to know
- Column names come back lowercased unless quoted (
formIDbecomesformid), as in every SQLFrame backend; Spark keeps their case. - Python UDFs (
spark.udf.register) can't run: the server would have to call back into your Python process. Usepyspark.sql.functionsor SQL. - In
spark.sql(), identifiers are quoted with backticks (the input dialect is Spark SQL, where double quotes are string literals). - Hardcharger runs DuckDB 2.0. Lambdas (
F.transform,F.filter, ...) are written in itsLAMBDA x : ...syntax; the arrow form older DuckDB took is refused there.
Coming from sqlframe-gizmosql
| sqlframe-gizmosql | sqlframe-hardcharger |
|---|---|
GizmoSQLSession, GizmoSQLDataFrame, ... |
HardchargerSession, HardchargerDataFrame, ... |
gizmosql.uri = grpc+tls://host:31337 or gizmosql://host |
hardcharger.uri = hardcharger://host |
gizmosql.username, .password, .tls_skip_verify, .auth_type |
the same names under hardcharger., plus access_token, tls_root_certs, client certificates and every other connect() argument |
gizmosql.max_msg_size |
gone: Warp SQL streams uploads over HTTP, with no message-size limit |
ingest() appends staged through a temporary table |
an ingest is one transaction; ingest_parts controls parallel upload |
🛠️ Development
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]" # plus adbc-driver-hardcharger from PyPI, or built from its repository
ruff check . && ruff format --check . && mypy
pytest tests/unit
SQLFRAME_HARDCHARGER_TEST_SERVER_BINARY=/path/to/hardcharger-server pytest -n 4
The live tests (everything outside tests/unit) start their own hardcharger-server
processes on free 127.0.0.1 ports, from the binary in SQLFRAME_HARDCHARGER_TEST_SERVER_BINARY;
without it they fail rather than skip. CI takes that binary from the
ghcr.io/gizmodata/hardcharger:main image. They are this package's own tests
(tests/integration) and SQLFrame's engine suite (tests/sqlframe_suite, vendored from
SQLFrame's release by scripts/vendor_sqlframe_suite.py and run against Hardcharger as SQLFrame
runs it against DuckDB). Every Python block in this README runs against a live server in CI
(scripts/run_readme_examples.py).
📦 Releasing
Semantic versioning and CHANGELOG.md. Pushing a vX.Y.Z
tag runs the full CI on the tagged commit, then builds the wheel and sdist and makes a GitHub
Release with the tag's CHANGELOG section.
📄 License
Apache License 2.0; see LICENSE. SQLFrame's engine tests in
tests/sqlframe_suite/upstream are MIT-licensed (see the LICENSE file there).
Built by GizmoData™
Hardcharger™ and GizmoData™ are trademarks of GizmoData LLC.
Metadata
Release files for sqlframe-hardcharger 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| sqlframe_hardcharger-2.0.0.tar.gz | 41.4 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sqlframe_hardcharger-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 86.5 kB
Release files / sqlframe_hardcharger-2.0.0.tar.gz
| Download URL | sqlframe_hardcharger-2.0.0.tar.gz |
|---|---|
| Size | 41.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b96dde367243a9f70e285d6caecf56ebff1ae0466385c7963b9321451f0ee1a2
|
|
BLAKE2b-256 checksum How to use checksums |
a5fe570172171c3e691253a2505563ab0112c94418f37b04524665b5e7b63501
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logRelease files / sqlframe_hardcharger-2.0.0-py3-none-any.whl
| Download URL | sqlframe_hardcharger-2.0.0-py3-none-any.whl |
|---|---|
| Size | 45.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
54780b6aa0196df5ef19995b1d220204e42bed5760614f4f5c043c6ed6d9c973
|
|
BLAKE2b-256 checksum How to use checksums |
a963a787ec2115fa8eb712f3a84d09095097128c725e4fe56663eee44b4bbf4c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logSecurity for sqlframe-hardcharger 2.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for sqlframe_hardcharger-2.0.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.