A Python 3.13 C extension that wraps the Advantage Database Server (ADS) Client Engine (ACE) SDK, exposing native ADS connectivity to Python via five clean classes and one exception type.
The API follows Python conventions throughout: snake_case methods, context managers (with blocks), iterators, and None returns instead of sentinel values.
| Component | Version |
|---|---|
| Python | 3.13.x — x64, Windows |
| Advantage Database Server | 11.x (ACE SDK 11.10) |
| Visual Studio | 2022 (v17.x, MSVC toolset 14.4x) |
| OS | Windows x64 (Server 2016 / 2019 / 2022 or Windows 10/11) |
No PE patching needed. Unlike the PHP variant, Python 3.13 was built with VS2022, so the output PYD is accepted by Python's loader without any linker-version fixup.
Python_ads/
├── src/
│ ├── ads.h # Shared header — object structs, type externs, helper prototypes
│ ├── ads_module.c # PyInit_ads, ads_set_error, field helpers, constants registration
│ ├── ads_connection.c # AdsConnection type
│ ├── ads_statement.c # AdsStatement type (iterator-capable)
│ ├── ads_table.c # AdsTable type (direct table access)
│ └── ads_misc.c # AdsTransaction + AdsDictionary types
├── tests/
│ └── test_ads.py # unittest suite (import tests run without a server)
├── test_env/ # Runtime: copy the PYD + ACE DLLs here to run tests
├── bin/ # Build output: ads.cp313-win_amd64.pyd
├── obj/ # Intermediate object files
├── Makefile.win # NMake build file
└── build.bat # One-shot build script (sets up VS2022 + NMake)
- Install Visual Studio 2022 (Community or Build Tools) with the Desktop development with C++ workload.
- Install Python 3.13 x64 for Windows (e.g. via the official installer to
%LOCALAPPDATA%\Programs\Python\Python313\). - The ACE SDK (
ace.h,ace64.lib) is shared fromF:\php_advantage\acesdk\— adjustACE_DIRinMakefile.winif your layout differs.
Open Makefile.win and verify these two variables match your installation:
PYTHON_DIR = C:\users\<you>\appdata\local\programs\python\python313
ACE_DIR = F:\php_advantage\acesdkOpen a regular Command Prompt (not a Developer Command Prompt — build.bat handles that):
cd F:\Python_ads
build.batOutput: bin\ads.cp313-win_amd64.pyd
To clean and rebuild:
build.bat clean && build.batCopy the PYD to any directory that is on sys.path, or to your project folder.
Option A — project-local (recommended for most use cases):
copy bin\ads.cp313-win_amd64.pyd <your_project>\Option B — user site-packages (available to all scripts):
python -c "import site; print(site.getusersitepackages())"
copy bin\ads.cp313-win_amd64.pyd %APPDATA%\Python\Python313\site-packages\Option C — system site-packages:
python -c "import site; print(site.getsitepackages()[0])"
copy bin\ads.cp313-win_amd64.pyd C:\users\...\Python313\Lib\site-packages\The ACE runtime DLLs must be on the Windows DLL search path when Python loads the extension. The simplest approach is to copy them into the same directory as the PYD:
copy acesdk\ace64.dll <install_dir>\
copy acesdk\adsloc64.dll <install_dir>\
copy acesdk\axcws64.dll <install_dir>\
copy acesdk\aicu64.dll <install_dir>\Alternatively, copy them to the Python executable's directory or any directory in the system PATH.
import ads
print(ads.AdsConnection) # <class 'ads.AdsConnection'>
print(ads.ADS_ADT) # 3cd F:\Python_ads
pip install pytest
:: Copy PYD + ACE DLLs to test_env\ first (done automatically after a clean build if
:: you run the commands below, or copy manually)
copy bin\ads.cp313-win_amd64.pyd test_env\
python -m pytest tests\test_ads.py -vTests that require a live ADS server or table file are skipped automatically when the paths are not reachable. Set environment variables to enable them:
set ADS_SERVER_PATH=\\192.168.0.10:6262\share\mydb.add
set ADS_TABLE_PATH=C:\data\mydb\customers.adt
set ADS_USER=admin
set ADS_PASSWORD=secret
python -m pytest tests\test_ads.py -vAll errors raise ads.AdsError (a subclass of Exception).
The code attribute is not set — inspect the message string for the native ACE error number.
ads.AdsConnection(
path, # str — server path or .add dictionary path
server_type = ads.ADS_REMOTE_SERVER, # int
user = "", # str
password = "", # str
options = ads.ADS_CHECKRIGHTS, # int
)| Method | Returns | Description |
|---|---|---|
close() |
None |
Disconnect. Safe to call multiple times. |
query(sql) |
AdsStatement |
Execute a SELECT, return statement object. |
execute(sql) |
None |
Execute INSERT / UPDATE / DELETE / DDL. |
begin_transaction() |
AdsTransaction |
Start a transaction. |
is_alive() |
bool |
Check whether the connection is still open. |
__enter__ / __exit__ |
— | Context manager; calls close() on exit. |
Returned by AdsConnection.query(). Supports the iterator protocol — use it directly in a for loop.
| Method | Returns | Description |
|---|---|---|
fetch_assoc() |
dict | None |
Next row as {field: value}, or None at EOF. |
fetch_row() |
list | None |
Next row as ordered list of values, or None at EOF. |
fetch_all() |
list[dict] |
All remaining rows as a list of dicts. |
column_count() |
int |
Number of columns in the result set. |
row_count() |
int |
Total number of records in the result set. |
close() |
None |
Release the statement handle. |
__iter__ / __next__ |
— | Iterator — yields one dict per row. |
__enter__ / __exit__ |
— | Context manager; calls close() on exit. |
Direct (non-SQL) table access.
ads.AdsTable.open(
path, # str — path to .adt / .dbf file
connection = None, # AdsConnection | None
table_type = ads.ADS_ADT, # int
lock_type = ads.ADS_COMPATIBLE_LOCKING, # int
char_set = ads.ADS_ANSI, # int
open_mode = ads.ADS_SHARED, # int
)| Method | Returns | Description |
|---|---|---|
close() |
None |
Close the table. |
goto_top() |
None |
Move to the first record. |
goto_bottom() |
None |
Move to the last record. |
goto_record(n) |
None |
Move to record number n. |
skip(n=1) |
None |
Skip n records forward (negative = backward). |
at_eof() |
bool |
True when past the last record. |
at_bof() |
bool |
True when before the first record. |
get_string(field) |
str |
Read a string / memo / date / timestamp field. |
get_long(field) |
int |
Read an integer / short / autoinc field. |
get_double(field) |
float |
Read a double / numeric / money / curdouble field. |
get_logical(field) |
bool |
Read a logical field. |
set_string(field, value) |
None |
Write a string field (buffer pending). |
set_long(field, value) |
None |
Write an integer field (buffer pending). |
set_double(field, value) |
None |
Write a float field (buffer pending). |
set_logical(field, value) |
None |
Write a logical field (buffer pending). |
get_record() |
dict |
All fields for the current record as {name: value}. |
record_count() |
int |
Total record count (ignoring filters). |
record_num() |
int |
Current physical record number. |
append_record() |
None |
Append a blank record and lock it for editing. |
write_record() |
None |
Flush all pending field changes to disk. |
cancel_update() |
None |
Discard pending changes without writing. |
delete_record() |
None |
Mark the current record as deleted. |
__enter__ / __exit__ |
— | Context manager; calls close() on exit. |
Returned by AdsConnection.begin_transaction().
| Method | Returns | Description |
|---|---|---|
commit() |
None |
Commit the transaction. |
rollback() |
None |
Roll back the transaction. |
is_active() |
bool |
True if the transaction has not been committed or rolled back. |
__enter__ / __exit__ |
— | Context manager: commits on success, rolls back on exception. |
Returned by AdsConnection.prepare(sql). Use parameterised queries whenever values come from user input or application variables — parameters are never interpreted as SQL.
Parameter syntax: write :name placeholders in the SQL string. The leading : is optional when calling bind methods — "status" and ":status" are equivalent.
| Method | Returns | Description |
|---|---|---|
bind(name, value) |
None |
Auto-detect Python type (None/bool/int/float/str/bytes) |
bind_string(name, value) |
None |
String / memo parameter |
bind_int(name, value) |
None |
Integer parameter |
bind_double(name, value) |
None |
Float parameter |
bind_bool(name, value) |
None |
Boolean parameter |
bind_date(name, value) |
None |
Date string "CCYYMMDD" |
bind_timestamp(name, value) |
None |
Timestamp "YYYY-MM-DD HH:MM:SS" |
bind_money(name, value) |
None |
SIGNED64 scaled integer (e.g. $10.00 = 100000 for 4dp fields) |
bind_binary(name, data, type=ADS_BINARY) |
None |
bytes object for BLOB/IMAGE/RAW fields |
bind_null(name) |
None |
SQL NULL |
execute() |
AdsStatement or True |
Run; SELECT → AdsStatement, DML/DDL → True |
param_count() |
int |
Number of parameters in the statement |
close() |
None |
Release the statement handle |
__enter__ / __exit__ |
— | Context manager; calls close() on exit |
bind() type mapping:
| Python type | ACE call |
|---|---|
None |
AdsSetNull |
bool |
AdsSetLogical |
int |
AdsSetLong |
float |
AdsSetDouble |
str |
AdsSetString |
bytes |
AdsSetBinary (ADS_BINARY) |
After
execute()on a SELECT, the statement handle transfers to the returnedAdsStatementand the prepared object is closed. Callprepare()again before the next execution.
Full CRUD access to the SAP ACE data dictionary. Wraps AdsDDxxx functions directly.
# Open independently (creates its own connection to the .add file)
ads.AdsDictionary.open(
path, # str
server_type = ads.ADS_REMOTE_SERVER, # int
user = "", # str
password = "", # str
options = ads.ADS_CHECKRIGHTS, # int
)
# Or borrow an existing connection (does not disconnect on close)
ads.AdsDictionary.from_connection(conn) # AdsConnection → AdsDictionary| Method | Returns | Description |
|---|---|---|
close() |
None |
Close (disconnect if owned). |
__enter__ / __exit__ |
— | Context manager; calls close() on exit. |
| Database | ||
get_database_property(prop) |
str |
Read a database-level DD property. |
set_database_property(prop, val) |
None |
Write a database-level DD property. |
| Tables | ||
add_table(alias, path, table_type=ADS_ADT, char_type=ADS_ANSI, index_path='', comment='') |
None |
Register a table in the dictionary. |
remove_table(alias, delete_files=False) |
None |
Remove a table from the dictionary. |
get_table_property(table, prop) |
str |
Read a table DD property. |
set_table_property(table, prop, val) |
None |
Write a table DD property. |
| Fields | ||
get_field_property(table, field, prop) |
str |
Read a field DD property. |
set_field_property(table, field, prop, val) |
None |
Write a field DD property. |
| Indexes | ||
add_index_file(table, index_path, comment='') |
None |
Associate an index file with a table. |
remove_index_file(table, index_path, delete_file=False) |
None |
Remove an index file association. |
get_index_property(table, index, prop) |
str |
Read an index DD property. |
set_index_property(table, index, prop, val) |
None |
Write an index DD property. |
| Users | ||
create_user(user, password='', group='', desc='') |
None |
Create a user account. |
delete_user(user) |
None |
Delete a user account. |
get_user_property(user, prop) |
str |
Read a user DD property. |
set_user_property(user, prop, val) |
None |
Write a user DD property. |
add_user_to_group(user, group) |
None |
Add a user to a group. |
remove_user_from_group(user, group) |
None |
Remove a user from a group. |
get_user_table_rights(user, table) |
int |
Return rights bitmask for a user on a table. |
set_user_table_rights(user, table, rights) |
None |
Set rights (revoke-all then grant). |
| Views | ||
create_view(name, sql, comment='') |
None |
Create a view. |
drop_view(name) |
None |
Drop a view. |
get_view_property(view, prop) |
str |
Read a view DD property. |
set_view_property(view, prop, val) |
None |
Write a view DD property. |
| Stored Procedures | ||
create_procedure(name, container, procedure, input='', output='', comment='') |
None |
Register a stored procedure. |
drop_procedure(name) |
None |
Remove a stored procedure. |
get_proc_property(name, prop) |
str |
Read a procedure DD property. |
set_proc_property(name, prop, val) |
None |
Write a procedure DD property. |
| Triggers | ||
create_trigger(name, table, trigger_type, event_types, container_type, container, procedure, priority=1, comment='', options=0) |
None |
Create a trigger. |
drop_trigger(name) |
None |
Drop a trigger. |
get_trigger_property(name, prop) |
str |
Read a trigger DD property. |
set_trigger_property(...) |
— | Raises AdsError — not supported by SAP ACE. |
| Referential Integrity | ||
create_ref_integrity(name, fail_table, parent, parent_tag, child, child_tag, update_rule=0, delete_rule=0) |
None |
Create a RI rule. |
remove_ref_integrity(name) |
None |
Remove a RI rule. |
| Links | ||
create_link(alias, path, user='', password='') |
None |
Create a cross-dictionary link. |
drop_link(alias) |
None |
Drop a link. |
modify_link(alias, path='', user='', password='') |
None |
Modify a link. |
try:
conn = ads.AdsConnection("\\\\bad_host\\nonexistent.add")
except ads.AdsError as e:
print(e) # "[5131] Cannot connect to server | <last-error detail>"| Category | Constants |
|---|---|
| Server type | ADS_LOCAL_SERVER, ADS_REMOTE_SERVER |
| Table type | ADS_NTX, ADS_CDX, ADS_ADT, ADS_VFP |
| Character set | ADS_ANSI, ADS_OEM |
| Open mode | ADS_SHARED, ADS_EXCLUSIVE |
| Locking | ADS_COMPATIBLE_LOCKING, ADS_PROPRIETARY_LOCKING |
| Rights | ADS_CHECKRIGHTS, ADS_IGNORERIGHTS |
| Filters | ADS_RESPECTFILTERS, ADS_IGNOREFILTERS |
| String trim | ADS_TRIM, ADS_LTRIM, ADS_RTRIM |
| Field types | ADS_LOGICAL, ADS_NUMERIC, ADS_DATE, ADS_STRING, ADS_MEMO, ADS_IMAGE, ADS_VARCHAR, ADS_DOUBLE, ADS_INTEGER, ADS_SHORTINT, ADS_TIME, ADS_TIMESTAMP, ADS_AUTOINC, ADS_RAW, ADS_CURDOUBLE, ADS_MONEY, ADS_ROWVERSION, ADS_MODTIME, ADS_NCHAR, ADS_NMEMO |
import ads
conn = ads.AdsConnection(
r"\\192.168.0.10:6262\share\mydb.add",
ads.ADS_REMOTE_SERVER,
"admin", "secret",
)
with conn.query("SELECT CustomerID, Name, Balance FROM customers ORDER BY Balance DESC") as stmt:
print(f"{stmt.column_count()} columns, {stmt.row_count()} rows")
for row in stmt:
print(f"{row['CustomerID']:<6} {row['Name'].strip():<30} {row['Balance']:>10.2f}")
conn.close()import ads
with ads.AdsConnection(r"\\server\share\mydb.add", ads.ADS_REMOTE_SERVER) as conn:
rows = conn.query("SELECT * FROM products WHERE active = TRUE").fetch_all()
for row in rows:
print(row)import ads
with ads.AdsConnection(r"\\server\share\mydb.add", ads.ADS_REMOTE_SERVER) as conn:
with conn.begin_transaction() as tx:
# commits automatically on success, rolls back on any exception
conn.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
conn.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2")import ads
with ads.AdsConnection(r"\\server\share\mydb.add", ads.ADS_REMOTE_SERVER) as conn:
with conn.query("SELECT PropertyID, Address, City FROM properties") as stmt:
rows = stmt.fetch_all()
for row in rows:
print(f"{row['PropertyID']}: {row['Address'].strip()}, {row['City'].strip()}")import ads
with ads.AdsTable.open(r"C:\data\inventory.adt") as tbl:
tbl.goto_top()
while not tbl.at_eof():
row = tbl.get_record()
print(f"{row['SKU']}: {row['Description']} — qty {row['Stock']}")
tbl.skip()import ads
with ads.AdsConnection(r"\\server\share\mydb.add", ads.ADS_REMOTE_SERVER) as conn:
with ads.AdsTable.open(r"C:\data\inventory.adt", connection=conn,
open_mode=ads.ADS_EXCLUSIVE) as tbl:
tbl.append_record()
tbl.set_string ("SKU", "WIDGET-42")
tbl.set_string ("Description", "Blue Widget")
tbl.set_double ("Price", 9.99)
tbl.set_long ("Stock", 100)
tbl.set_logical("Active", True)
tbl.write_record()import ads
with ads.AdsConnection(r"\\server\share\mydb.add", ads.ADS_REMOTE_SERVER,
"admin", "secret") as conn:
with conn.prepare(
"SELECT id, name, balance FROM customers "
"WHERE status = :status AND balance > :minbal ORDER BY balance DESC"
) as prep:
prep.bind_string("status", "ACTIVE")
prep.bind_double("minbal", 1000.00)
stmt = prep.execute() # returns AdsStatement
for row in stmt:
print(f"{row['id']:<6} {row['name'].strip():<30} {row['balance']:>10.2f}")
stmt.close()import ads
with ads.AdsConnection(r"\\server\share\mydb.add", ads.ADS_REMOTE_SERVER) as conn:
products = [
("WIDGET-01", "Alpha Widget", 9.99, 100),
("WIDGET-02", "Beta Widget", 14.99, 50),
("WIDGET-03", "Gamma Widget", 4.99, 200),
]
with conn.begin_transaction(): # auto-commits; rolls back on exception
for sku, desc, price, stock in products:
prep = conn.prepare(
"INSERT INTO products (sku, description, price, stock) "
"VALUES (:sku, :desc, :price, :stock)"
)
prep.bind(":sku", sku) # str → AdsSetString
prep.bind(":desc", desc)
prep.bind(":price", price) # float → AdsSetDouble
prep.bind(":stock", stock) # int → AdsSetLong
prep.execute()
print(f"{len(products)} products inserted.")import ads
with ads.AdsConnection(r"\\server\share\mydb.add", ads.ADS_REMOTE_SERVER) as conn:
prep = conn.prepare(
"UPDATE customers SET phone = :phone, fax = :fax WHERE id = :id"
)
prep.bind("phone", "+1-555-0100") # str → AdsSetString
prep.bind("fax", None) # None → AdsSetNull
prep.bind("id", 42) # int → AdsSetLong
prep.execute()import ads
# Open via its own connection
with ads.AdsDictionary.open(r"\\server\share\mydb.add", ads.ADS_REMOTE_SERVER) as dd:
path = dd.get_table_property("customers", 1) # ADS_DD_TABLE_PATH = 1
print(f"customers table is at: {path}")
# Or borrow an existing connection
with ads.AdsConnection(r"\\server\share\mydb.add", ads.ADS_REMOTE_SERVER) as conn:
dd = ads.AdsDictionary.from_connection(conn)
ver = dd.get_database_property(1) # ADS_DD_VERSION = 1
print(f"Dictionary version: {ver}")import ads
with ads.AdsDictionary.open(r"\\srv\data\mydb.add", ads.ADS_REMOTE_SERVER,
"admin", "secret") as dd:
# Register an existing table
dd.add_table("invoices", r"C:\data\invoices.adt")
dd.set_table_property("invoices", 3, "Customer invoice table") # ADS_DD_TABLE_DESCRIPTION
# Create a view
dd.create_view("open_invoices",
"SELECT id, customer_id, amount FROM invoices WHERE paid = FALSE",
"Unpaid invoices")
# Register a stored procedure
dd.create_procedure("sp_close_invoice", "procs.dll", "CloseInvoice",
"@invoice_id INTEGER", "", "Mark invoice as paid")
# Create a user and grant read access
dd.create_user("reports_user", "rp@ss", "readers", "Read-only reporting account")
dd.set_user_table_rights("reports_user", "invoices", 0x0001) # ADS_READ_RIGHT
# Referential integrity: invoices.customer_id → customers.id
dd.create_ref_integrity("ri_inv_cust", "ri_errors",
"customers", "cust_pk",
"invoices", "inv_fk")
# Cross-dictionary link to HR database
dd.create_link("hr_db", r"\\hrserver\data\hr.add", "readonly", "hrpass")import ads
try:
conn = ads.AdsConnection(r"\\bad_host\nonexistent.add")
except ads.AdsError as e:
print(f"Connection failed: {e}")
# Field-level errors
with ads.AdsTable.open(r"C:\data\mytable.adt") as tbl:
tbl.goto_top()
try:
val = tbl.get_string("NONEXISTENT_FIELD")
except ads.AdsError as e:
print(f"Field error: {e}")| ACE field type | Python type returned |
|---|---|
ADS_LOGICAL |
bool |
ADS_INTEGER, ADS_SHORTINT, ADS_AUTOINC |
int |
ADS_DOUBLE, ADS_NUMERIC, ADS_MONEY, ADS_CURDOUBLE |
float |
ADS_STRING, ADS_VARCHAR, ADS_NCHAR |
str (UTF-8 decoded, trailing spaces trimmed) |
ADS_MEMO, ADS_NMEMO |
str |
ADS_DATE, ADS_TIMESTAMP, ADS_TIME |
str (raw ADS string representation) |
ADS_RAW, ADS_IMAGE, all others |
str |
| NULL / read error | None |
| Symptom | Cause | Fix |
|---|---|---|
ImportError: DLL load failed while importing ads |
ACE DLLs not found | Copy ace64.dll, adsloc64.dll, axcws64.dll, aicu64.dll alongside the PYD |
ImportError: dynamic module does not define module export function |
Wrong Python version | The PYD is built for Python 3.13; check python --version |
ads.AdsError: [5131] Cannot connect ... |
Server unreachable or wrong path | Verify ADS server is running and the UNC path / port is correct |
ads.AdsError: table is closed |
Method called after close() |
Don't re-use a closed object; open a new one |
Build: C1083: Cannot open include file: 'Python.h' |
PYTHON_DIR wrong in Makefile.win |
Set PYTHON_DIR to the actual Python 3.13 install root |
Build: C1083: Cannot open include file: 'ace.h' |
ACE_DIR wrong in Makefile.win |
Set ACE_DIR to the directory containing ace.h |
Build: LNK2019: unresolved external symbol PyInit_ads |
Module init function missing | Check ads_module.c defines PyMODINIT_FUNC PyInit_ads(void) |
PHP (php_ads) |
Python (ads) |
|
|---|---|---|
| Method style | camelCase (fetchAssoc, isAlive) |
snake_case (fetch_assoc, is_alive) |
| Connection | AdsConnection::connect(array) static factory |
ads.AdsConnection(path, ...) constructor |
| Table open | AdsTable::open(AdsConnection, path, ...) |
ads.AdsTable.open(path, connection=None, ...) |
| Statement iteration | manual while fetchAssoc() !== false |
native for row in stmt: |
| Transaction context | manual commit/rollback | with conn.begin_transaction() as tx: auto-commits |
| Error type | AdsException extends RuntimeException |
ads.AdsError extends Exception |
| PE patching | required (VS2022 → VS2019 linker ver) | not required |
This extension is provided as-is for use with licensed copies of Advantage Database Server.
The ACE SDK headers and libraries (acesdk/) are copyright SAP / iAnywhere Solutions and
are included under the terms of the ADS developer license — they must not be redistributed
separately.