Using the SQLAlchemy pyami-3forge Dialect¶
pyami.db.sqlalchemy_ami is a SQLAlchemy dialect for 3forge Enterprise. It lets you drive an
AmiCore server with SQLAlchemy Core and the ORM, and plug into the wider ecosystem
(pandas, Alembic-style reflection, etc.). Under the hood it routes through
pyami.db.ami_dbapi (a PEP 249 / DB-API 2.0 wrapper) which talks to the native
pyami.rt.ami_client extension over the AMI JDBC port.
For the broader API and the non-SQLAlchemy layers, see the user guide.
Install & Register¶
The dialect ships inside pyami-3forge:
It registers itself automatically through the sqlalchemy.dialects entry point
(declared in pyproject.toml), so once the package is installed the ami+jdbc:// URL
scheme just works: no manual step.
If you run from a source checkout without installing the package, register it by hand once per process:
Connection URL¶
| Query parameter | Default | Description |
|---|---|---|
use_ssl |
true |
SSL/TLS for the JDBC connection. Pass false/0/no to use plain TCP. |
timeout_ms |
60000 |
Query timeout in milliseconds. |
limit |
10000 |
Maximum rows returned per query. |
Parameter names matter. Query parameters are forwarded verbatim as keyword arguments to the DB-API
connect(), whose names areuse_ssl,timeout_ms, andlimit. Usetimeout_ms, nottimeout(an unknown keyword raisesTypeErrorat connect time).
SSL is on by default, so a bare URL connects over SSL:
For backup JDBC servers (failover), connect through the DB-API layer or the realtime client directly: see DB-API 2.0.
AMI SQL Differences You Will Notice¶
The dialect translates most standard SQLAlchemy constructs to AMI SQL automatically (see
Operator & Function Translation). But when you write
raw text() you must use AMI syntax yourself:
| Standard SQL | AMI SQL |
|---|---|
= (equality) |
== |
'string' |
"string" (double quotes) |
LIKE 'p%' |
~~ "/^p.*/" |
SELECT DISTINCT … |
SELECT … GROUP BY … |
LIMIT n OFFSET m |
LIMIT m, n |
CAST(x AS INT) |
(INT)(x) |
NOW() |
timestamp() |
~~ is a filter match, not LIKE. Its wildcards are *, for any run of characters, and ., for any single
character; % and _ are ordinary characters. By default the match is partial and case-insensitive, so
~~ "p%" looks for the literal text p%. A pattern written between slashes is a regular expression:
~~ "/^p.*/" is anchored and case-sensitive, which is how LIKE 'p%' behaves.
Tables are created PUBLIC by default (the DDL compiler injects the PUBLIC prefix if you
do not specify one). AMI CREATE TABLE does not accept PRIMARY KEY / UNIQUE /
NOT NULL / FOREIGN KEY clauses. The dialect omits PRIMARY KEY, NOT NULL, and FOREIGN KEY, and
replaces the primary key and foreign keys with separate CREATE INDEX ... USE CONSTRAINT=... statements (see
DDL); NOT NULL is dropped with no replacement. UNIQUE is not omitted: a
column declared with unique=True, or a UniqueConstraint, is written into the CREATE TABLE statement,
which the Center rejects. Declare uniqueness instead with an Index that has unique=True or
info={"use_constraint": "UNIQUE"}.
Core: Raw SQL¶
Bind parameters use the named paramstyle (:name):
commit()/rollback()are no-ops (Enterprise has no transactions). Each statement is applied as it executes; do not rely on rollback semantics. See Limitations.
Core: Tables & Expressions¶
users.c.id == 2 compiles to AMI SQL's id == :id_1; you write normal SQLAlchemy and the
dialect emits AMI operators.
ORM¶
DDL, Constraints & Indexes¶
Because AMI CREATE TABLE rejects standard constraint clauses, the dialect:
- Omits
PRIMARY KEY/NOT NULL/FOREIGN KEYfrom the column/table DDL. - Registers a
Table.after_createhook that creates indexes for the primary key and foreign keys only: - primary key →
CREATE INDEX idx_<table>_pk ON <table>(...) USE CONSTRAINT="PRIMARY" - foreign keys →
CREATE INDEX idx_<table>_fk ON <table>(...) USE CONSTRAINT="NONE"
NOT NULL has no replacement. UNIQUE is not omitted, so declare it with an Index (below) rather than
with unique=True on a column.
For a single-column primary key this is automatic: declaring a primary_key=True column is enough. The
hook has 2 gaps, and neither raises an error:
- Composite primary key: gets no index, and the table's foreign-key index is then skipped as well.
Declare the primary-key index yourself with an
Indexandinfo={"use_constraint": "PRIMARY"}. - Several foreign keys: only one is indexed, and which one is not fixed. Declare an
Indexfor each foreign key you need indexed.
Index constraint: choose the constraint kind explicitly on an Index:
Per-column index modifiers: (HASH, SORT, SERIES) 2 equivalent ways:
Autogenerated values: (AUTOGEN="INC"|"RAND") on the primary-key column (propagates to
the generated PK index) or directly on an index:
Indexes are emitted as CREATE INDEX IF NOT EXISTS <name> ON <table>(<cols [MODIFIER]>)
[USE CONSTRAINT="…"] [AUTOGEN="…"].
Operator & Function Translation¶
The statement compiler rewrites these for you (so the same SQLAlchemy expression works against AmiCore):
| SQLAlchemy | AMI SQL emitted |
|---|---|
col == value |
col == :value |
col != value |
col != :value |
col.like("x%") |
col ~~ :value |
col.notlike("x%") |
NOT (col ~~ :value) |
select(...).distinct() |
SELECT … GROUP BY … |
select(...).limit(n).offset(m) |
… LIMIT :m, :n |
cast(col, Integer) |
(INT)(col) |
func.now() |
timestamp() |
like, ilike, startswith, endswith, contains, and their negations all go through ~~. The dialect
converts the pattern to an anchored regular expression before binding it, with % becoming .* and _
becoming .: ilike and its variants are case-insensitive, and the others are case-sensitive. A negation is
emitted as NOT (col ~~ :value), not !~, because the Center's !~ matches every row when given a regular
expression. The pattern must be a value: like against another column cannot be translated and raises a
compile error.
String literals are quoted with double quotes ("...") and identifiers with backticks (`...`), matching
AMI SQL, where a double-quoted token is always a string literal.
Type Mapping¶
When the dialect emits DDL, SQLAlchemy types map to AMI types as follows:
| SQLAlchemy type | AMI type (DDL) |
|---|---|
Integer |
INT |
SmallInteger |
SHORT |
BigInteger |
LONG |
Float |
DOUBLE |
REAL |
FLOAT |
String / Unicode / Text |
STRING |
CHAR |
CHAR |
Boolean |
BOOLEAN |
DateTime |
UTC |
BINARY |
Binary |
LargeBinary |
Binary |
For reflection (reading types back from the server), AMI type names map to SQLAlchemy
types via the dialect's ischema_names: e.g. Double→Float, Long/BigDecimal→
BigInteger, Short→SmallInteger, UTC/UTCN→DateTime, UUID→a UUID type,
Complex→a complex type.
pandas Integration¶
Read straight into a DataFrame through a SQLAlchemy connection:
(For a lighter-weight query-to-DataFrame path that skips SQLAlchemy entirely, the native
client's sql() and the pure-Python pyjdbc client both return DataFrame-ready output:
see the user guide.)
Reflection / Inspection¶
inspect(engine) works against AmiCore's system tables (__TABLE, __COLUMN, __INDEX):
has_table() and MetaData(...).reflect() are likewise supported, so checkfirst=True on
create_all/drop_all does the right thing.
Limitations¶
- No transactions:
commit()androllback()are no-ops, so statements take effect as they execute (skip_autocommit_rollbackis set on the dialect). - One result set per statement: from a multi-result AmiCore response, only the first result set is returned.
- JDBC protocol only: the dialect does not cover publish/subscribe or Center feeds, so use
pyami.rt.ami_clientdirectly for those.
Troubleshooting¶
| Symptom | Cause / fix |
|---|---|
TypeError: ... unexpected keyword argument 'timeout' |
You used ?timeout= in the URL. The correct parameter is ?timeout_ms=. |
Can't load plugin: sqlalchemy.dialects:ami.jdbc |
Package not installed (entry point missing). Either pip install pyami-3forge, or registry.register("ami.jdbc", "pyami.db.sqlalchemy_ami.base", "AmiDialect"). |
Could not parse SQLAlchemy URL from given URL string |
The URL is malformed, e.g: ami+jdbc// with the colon missing. Check it against the connection URL format. |
| SSL handshake / connection failures | Server is not on SSL, or wrong port. Add ?use_ssl=false or correct the port. |
commit() "does not persist a rollback" |
Expected: there are no transactions; every statement is immediately applied. |
Syntax error on raw text() SQL |
You used standard SQL (=, '…', LIKE). Use AMI syntax (==, "…", ~~). |
CREATE TABLE rejected for constraints |
A UNIQUE clause reached the DDL, from unique=True on a column or a UniqueConstraint, or PRIMARY KEY/UNIQUE was written inline in raw DDL. Declare primary keys with primary_key=True and uniqueness with an Index, or write CREATE INDEX ... USE CONSTRAINT=... yourself. |
See also: the user guide (full API, DB-API layer, AMI SQL notes) and the Jupyter guide (interactive use, pandas, live feeds).