frostlake-sqlalchemy
A SQLAlchemy dialect for Frostlake,
riding the pure-stdlib frostlake DB-API driver over
the engine's HTTP protocol. Core, ORM and reflection all work; no JVM, no JDBC.
from sqlalchemy import create_engine
engine = create_engine("frostlake://localhost:18082/MY_DB?schema=PUBLIC")
Requirements
- SQLAlchemy 2.0 or newer
- the
frostlakedriver 0.3.0 or newer (installed as a dependency) - a Frostlake engine 0.2.0 or newer, reachable over HTTP
Install
pip install frostlake-sqlalchemy
Connection URL
frostlake://[host[:port]][/DATABASE][?schema=SCHEMA&timeout=SECONDS]
Host defaults to localhost, port to 18082. The database and schema become USE
statements on the session before the first statement. The protocol carries no
credentials, so a username or password in the URL is ignored (with a warning).
create_engine("frostlake://") # localhost:18082
create_engine("frostlake://db.internal:18082/ANALYTICS?schema=STAGING")
Quick start
from sqlalchemy import Column, Integer, MetaData, Sequence, String, Table, select
md = MetaData()
people = Table(
"people", md,
Column("id", Integer, Sequence("people_id_seq"), primary_key=True),
Column("name", String(50), nullable=False, comment="display name"),
)
md.create_all(engine)
with engine.begin() as conn:
conn.execute(people.insert(), [{"name": "ada"}, {"name": "alan"}])
for row in conn.execute(select(people).order_by(people.c.name)):
print(row.id, row.name)
Identifier case
Frostlake follows Snowflake: an unquoted identifier folds to upper case, a quoted one keeps its case. The dialect normalizes both ways, so lower-case names in Python address the upper-case objects in the database, and reflection hands lower-case names back:
| In Python | In SQL | Stored as |
|---|---|---|
Table("people", ...) |
people |
PEOPLE |
Table("People", ...) |
"People" |
People |
The 64 words the engine actually rejects in an identifier position are quoted
automatically; they are listed in frostlake_sqlalchemy.RESERVED_WORDS.
Types
| SQLAlchemy | Frostlake | Notes |
|---|---|---|
Integer, BigInteger, SmallInteger |
INTEGER, BIGINT, SMALLINT |
all stored as NUMBER(38, 0) |
Numeric(p, s), NUMBER(p, s) |
NUMBER(p, s) |
exact; reads back as Decimal |
Float, Double, REAL |
FLOAT, DOUBLE, REAL |
one binary float type underneath |
String(n), Text, Unicode |
VARCHAR(n), VARCHAR |
|
Boolean |
BOOLEAN |
native |
Date, Time |
DATE, TIME |
|
DateTime |
TIMESTAMP_NTZ |
DateTime(timezone=True) gives TIMESTAMP_TZ |
TIMESTAMP_NTZ, TIMESTAMP_LTZ, TIMESTAMP_TZ |
same | for picking one explicitly |
LargeBinary |
BINARY |
reads back as bytes |
JSON, VARIANT, OBJECT, ARRAY |
VARIANT, OBJECT, ARRAY |
see below |
GEOGRAPHY, GEOMETRY |
same | handled as text |
Enum |
VARCHAR(n) |
no native enum |
Uuid |
CHAR(32) |
no native uuid |
The Frostlake-specific ones import from the package:
from frostlake_sqlalchemy import VARIANT, OBJECT, ARRAY, TIMESTAMP_LTZ
Note frostlake_sqlalchemy.ARRAY shadows sqlalchemy.ARRAY — they model different
things. Frostlake's is an untyped semi-structured array; SQLAlchemy's generic one is a
typed, dimensioned SQL array the engine has no equivalent for.
Semi-structured data
VARIANT, OBJECT and ARRAY behave like SQLAlchemy's JSON type — documents go in as
Python objects and come back as Python objects, and col["key"] indexing works
(compiled to GET() / GET_PATH()). Column(JSON) is a VARIANT column.
One limitation, inherited from the engine: a VALUES clause takes no function calls,
and a VARIANT value needs PARSE_JSON() around it. So a semi-structured column has to be
written with INSERT ... SELECT:
# works
conn.execute(docs.insert().from_select(
["id", "payload"],
select(literal(1), literal({"k": [1, 2]}, VARIANT))))
# rejected by the engine: INSERT INTO docs (id, payload) VALUES (?, PARSE_JSON(?))
conn.execute(docs.insert().values(id=1, payload={"k": [1, 2]}))
Reading is unrestricted:
conn.execute(select(docs.c.payload["k"])) # GET(docs.payload, 'k')
conn.execute(select(docs.c.payload[("a", "b")])) # GET_PATH(docs.payload, 'a.b')
Primary keys
Frostlake has no RETURNING and no last-insert-id, so a server-generated key cannot be
read back after an INSERT. Give an ORM primary key a Sequence: the dialect fetches
the next value before the insert, so the object comes back with its key populated.
class Person(Base):
__tablename__ = "people"
id = Column(Integer, Sequence("people_id_seq"), primary_key=True)
name = Column(String(30))
Without one, an integer primary key gets AUTOINCREMENT in the DDL and the engine fills
it in — fine for Core inserts and bulk loads, but the ORM will not know the value it was
given. Identity(start=..., increment=...) is supported too, with the same caveat.
Table options
Table("events", md,
Column("day", Date),
Column("kind", String(20)),
frostlake_clusterby=["day", "kind"]) # CLUSTER BY (day, kind)
Inspector.get_table_options() reflects it back. Table and column comment= are
supported in both directions.
Reflection
get_table_names, get_view_names, get_view_definition, get_columns,
get_pk_constraint, get_foreign_keys, get_unique_constraints, get_table_comment,
get_table_options, get_schema_names, get_sequence_names, has_table,
has_sequence — plus Table(..., autoload_with=engine) and MetaData.reflect().
A schema may be given as "database.schema" to reach another database:
inspect(engine).get_table_names(schema="OTHER_DB.PUBLIC")
Table("t", md, autoload_with=engine, schema="OTHER_DB.PUBLIC")
Constraints the engine named itself (SYS_CONSTRAINT_<uuid>) are reported with
name: None, so a reflected table does not carry one server's random names.
Two gaps worth knowing:
get_indexes()is always[]andget_check_constraints()is always[]— the engine parsesCHECKbut does not store it inINFORMATION_SCHEMA.CHECK_CONSTRAINTS.- identity seed and step are read from
DESCRIBE TABLE, becauseINFORMATION_SCHEMA.COLUMNSreports1/1for every identity column on engine 0.0.7.
Transactions
Connections are transactional the way SQLAlchemy expects: engine.begin(),
connection.commit() and connection.rollback() all work. Two isolation levels are
accepted:
with engine.connect().execution_options(isolation_level="AUTOCOMMIT") as conn:
...
READ COMMITTED is the default. There are no savepoints, so
Session.begin_nested() and connection.begin_nested() raise NotImplementedError.
What the engine does not have
| Feature | Behaviour |
|---|---|
| indexes | CreateIndex raises CompileError; drop index=True from your models |
| savepoints / nested transactions | NotImplementedError |
SELECT ... FOR UPDATE |
the clause is dropped; there is no row locking |
RETURNING |
not emitted; use a Sequence for generated keys |
DEFERRABLE constraints |
the clause is not emitted |
INSERT with no values at all |
needs at least one named column — same as Oracle |
INTERVAL columns |
no such column type |
Two Snowflake-compatible behaviours that differ from most other backends:
regexp_match()compiles toRLIKE, which matches the whole value."a.a"matchesada;"a"does not. Anchor-free searching needs.*on both ends.10 / 3is3.333333, not3— division is never integer division.
Tests
pip install -e ".[test]"
pytest tests/test_compile.py # no server, no JVM
The live tests boot an engine themselves from a classpath:
JAVA_HOME=~/.jdks/liberica-17 FROSTLAKE_CLASSPATH="<engine jar + deps>" pytest
With FROSTLAKE_CLASSPATH unset the live tests skip and the compile tests still run, so a
missing engine never shows up as a false pass.
Set FL_CORPUS to the engine's testkit directory (an absolute path) and that run also
replays the engine's language-neutral SQL corpus through the dialect (testkit_runner.py);
without it, that test skips:
FL_CORPUS=/path/to/frostlake/engine/src/test/resources/testkit \
JAVA_HOME=~/.jdks/liberica-17 FROSTLAKE_CLASSPATH="<engine jar + deps>" pytest
License
Apache-2.0.
Metadata
Release files for frostlake-sqlalchemy 0.2.1
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 | Actions |
|---|---|---|---|---|
| frostlake_sqlalchemy-0.2.1.tar.gz | 32.9 kB | Without provenance | Details for frostlake_sqlalchemy-0.2.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| frostlake_sqlalchemy-0.2.1-py3-none-any.whl | Python 3 | none | any | Details for frostlake_sqlalchemy-0.2.1-py3-none-any.whl |
Total release size: 54.3 kB
Release files / frostlake_sqlalchemy-0.2.1.tar.gz
| Download URL | frostlake_sqlalchemy-0.2.1.tar.gz |
|---|---|
| Size | 32.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
34b1ad8e17c8d522eaaa36ff1ce2c4b8f084ed28dd74bcd027e4a2cdd1e9301c
|
|
BLAKE2b-256 checksum How to use checksums |
987974a351ea1bf53e1cac745a6f9a300198b1e62aa9079a0d9995687636dc67
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Release files / frostlake_sqlalchemy-0.2.1-py3-none-any.whl
| Download URL | frostlake_sqlalchemy-0.2.1-py3-none-any.whl |
|---|---|
| Size | 21.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4e0ec1c2e3e91e33ecbe2aa0c7184c36b0b63355a48c94e91404dd04c71df576
|
|
BLAKE2b-256 checksum How to use checksums |
d21e6200e4541f5c26e2dcd3f892bac4dcfde8a73d56d1370c181273a4abe5b6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Security for frostlake-sqlalchemy 0.2.1
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A 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.
To learn more about protecting yourself from malicious actors, visit our package security documentation.