Skip to content

reinaldocrespo/python_ads

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ads — Python extension for Advantage Database Server

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.


Requirements

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.


Directory layout

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)

Building from source

1. Prerequisites

  • 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 from F:\php_advantage\acesdk\ — adjust ACE_DIR in Makefile.win if your layout differs.

2. Adjust paths (if needed)

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\acesdk

3. Build

Open a regular Command Prompt (not a Developer Command Prompt — build.bat handles that):

cd F:\Python_ads
build.bat

Output: bin\ads.cp313-win_amd64.pyd

To clean and rebuild:

build.bat clean && build.bat

4. Install

Copy 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\

5. Copy ACE runtime DLLs

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.

6. Verify

import ads
print(ads.AdsConnection)   # <class 'ads.AdsConnection'>
print(ads.ADS_ADT)         # 3

Running the tests

cd 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 -v

Tests 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 -v

API reference

All 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

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.

ads.AdsStatement

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.

ads.AdsTable

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.

ads.AdsTransaction

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.

ads.AdsPreparedStatement

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 returned AdsStatement and the prepared object is closed. Call prepare() again before the next execution.


ads.AdsDictionary

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.

ads.AdsError

try:
    conn = ads.AdsConnection("\\\\bad_host\\nonexistent.add")
except ads.AdsError as e:
    print(e)   # "[5131] Cannot connect to server | <last-error detail>"

Registered constants

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

Usage examples

Connect and run a SQL query

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()

Context manager (auto-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)

Transaction with auto-rollback on exception

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")

Fetch all rows into a list of dicts

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()}")

Direct table navigation

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()

Append a new record

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()

Parameterised SELECT

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()

Parameterised INSERT inside a transaction

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.")

Auto-bind with None (NULL)

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()

Data dictionary — read properties

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}")

Data dictionary — full CRUD example

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")

Error handling

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}")

Type mapping

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

Troubleshooting

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)

Differences from the PHP extension

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

License

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.


See also

About

Python extension for full use to Advantage Database Server (SAP ADS ACE)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors