Skip to main content

Basic SQL via Python: Using InterSystems IRIS as a Relational Database

As a Python developer, you are likely familiar with querying databases using SQL. InterSystems IRIS® data platform supports this workflow seamlessly, allowing you to treat it like a high-performance relational database using familiar Python tools. However, if you truly want to work with relational data using Python’s object-oriented nature, you can use an Object Relational Mapping (ORM), which converts data representation between a relational database and an object-oriented programming language.

This section introduces two common data manipulation techniques in Python that can be used with InterSystems IRIS:

  • DB-API: A lightweight, direct SQL interface.

  • SQLAlchemy: A powerful ORM and SQL toolkit.

Prerequisites

Before you begin, you will need to install the official InterSystems IRIS DB-API driver (intersystems-irispython). Make sure to install the following packages:

pip install intersystems-irispython
pip install sqlalchemy-iris
Note:

sqlachemy-iris will automatically install sqlachemy as a dependency.

If you previously installed intersystems-irispython you made need to force an update:

pip install intersystems-irispython==<version>

If you are using a local InterSystems IRIS installation, you may also need to install the intersystems-irispython wheel (.whl) file that comes with InterSystems IRIS. This ensures that the DB-API driver is installed.

pip install intersystems-irispython-<version>.whl

This .whl file is typically located in the dev/python directory of your InterSystems IRIS installation (for example, C:\InterSystems\training\dev\python). To install it:

  1. Open a command line.

  2. cd into the bin directory of your InterSystems IRIS installation (for example, C:\InterSystems\training\bin).

  3. Run pip install pointing to the .whl file.

Note:

The DB-API driver (intersystems-irispython) is required even when using SQLAlchemy, as it provides the underlying connection.

DB-API: Direct SQL Access

The iris module provides a PEP 249–compliant interface for executing raw SQL queries. DB-API is the standard interface to interact with any relational backend. It is ideal for lightweight scripts, data access layers, and quick prototyping.

Note:

There are multiple iris modules, each with their own APIs. This section focuses on the DB-API approach, which is used for external Python applications that connect to InterSystems IRIS.

Establishing a DB-API Connection

To establish a connection to an InterSystems IRIS instance using DB-API, use the iris.connect() method. This code creates a connection to the InterSystems IRIS instance and opens a cursor for executing SQL commands.

import iris

# Replace with your connection details
connection_string = "localhost:1972/USER"
username = "_system"
password = "SYS"
connection = iris.connect(connection_string, username, password)

cursor = connection.cursor()
Note:

In connection_string, 1972 is the port number while USER is the namespace that you connect to. Change these to match your specific needs, as well.

Remember to close the cursor and connection when you are done:

cursor.close()
connection.close()

Executing a SQL Query

Once connected, you can execute SQL queries using the cursor object. You can then retrieve the results from the query using the commands fetchone(), fetchmany(), fetchall(), or scroll().

cursor.execute("SELECT * FROM Sample.Person WHERE Age >= 50")

for row in cursor.fetchall():
    print(row)

Parameters

Parameters help prevent SQL injections and can make your queries more flexible. With DB-API, you can specify both positional and named parameters to extend your queries. Pass the parameters along with the SQL statement to the cursor to execute them.

Positional Parameters

Positional parameters match the question marks in the SQL statement with the arguments in the parameters list by position.

sql = "SELECT * FROM Sample.Person WHERE ID = ? and Name = ?"
params = [1, 'Doe, John']
cursor.execute(sql, params)
result = cursor.fetchone()
row = result[:]
print(row)

Named Parameters

Named parameters match the :argument variables in the SQL statement with the arguments in the parameters dictionary by keyword.

sql = "SELECT * FROM Sample.Person WHERE ID = :id and Name = :name"
params = {'id' : '1', 'name' : 'Doe, John'}
cursor.execute(sql, params)
result = cursor.fetchone()
row = result[:]
print(row)

For more documentation on DB-API, see Using the Python DB-APIOpens in a new tab.

SQLAlchemy: SQL Toolkit and ORM

SQLAlchemy is a powerful SQL toolkit and ORM tool built on top of DB-API. It provides a higher-level abstraction over SQL, allowing you to define tables as Python classes and interact with them using ORM. It is ideal for larger applications that require cleaner, more maintainable code and helps you avoid raw SQL when possible.

SQLAlchemy has two major components:

  1. Core—for executing raw SQL and building SQL expressions.

  2. ORM—for mapping Python classes to database tables and managing transactions.

Establishing an Engine Connection

To connect to InterSystems IRIS using SQLAlchemy, use the create_engine() function with the appropriate dialect and connection string. The dialect is specified before the :// in the DATABASE_URL. The engine is the central source of connection and is used for both the Core and ORM.

from sqlalchemy import create_engine

# Replace with your credentials and connection information
username = "_SYSTEM"
password = "SYS"
namespace = "USER"
DATABASE_URL = f"iris://{username}:{password}@localhost:1972/{namespace}"

engine = create_engine(DATABASE_URL, echo=True)	# Set echo=True to see the SQL queries being executed in the terminal
Note:

You can also use iris+emb:// or iris+intersystems as the dialect to use Embedded PythonOpens in a new tab (to run Python in the same process as InterSystems IRIS) or the official InterSystems official driver, respectively. See sqlalchemy-irisOpens in a new tab for more details.

Just like in DB-API, you can execute simple static queries via the .execute() method:

# SQL statements get wrapped in text sequences
from sqlachemy import text

# Connect to the database and execute a static query
with engine.connect() as conn:
    query = text("SELECT * FROM Sample.Person WHERE ID = 1")
    result = conn.execute(query)
    row = result.fetchone()
    print(row)

Transactions and Sessions

InterSystems IRIS supports robust transaction management through SQLAlchemy, whether you are using the Core or ORM interface. Through this, you have control over transaction states, commits, roll backs, and much more.

engine.begin()

The engine.begin() method is a context manager that starts a transaction. It starts a transaction block that automatically commits when the block exists without an error.

# engine.begin starts a transaction block with an auto-commit
with engine.begin() as conn:
    result = conn.execute(text("SELECT Name, Age, FROM Sample.Person"))
    for row in result:
        print(f"Name: {row.Name}\nAge: {row.Age}\n")

engine.connect()

The engine.connect() starts a transaction block that automatically rolls back when the block exists. To save any changes made in the transaction, you must call to conn.commit() to commit your changes.

# engine.connect starts a transaction block with an automatic roll back
with engine.connect() as conn:
    conn.execute(demo_table.insert(), [{"id": 1, "value": "Test"}],)
    conn.commit()		# Need to explictly commit to save changes

Session(engine)

Then Session object provides a high-level interface for managing transactions and interacting with ORM objects. It establishes a conversation between the database and your code, giving you fine-grained control over commits, rollbacks, and object states.

# Session starts an ORM session with fine-grained commit/rollback controls
from sqlalchemy.orm import Session

print("Listing rows where Age > 50:")
stmt = text("SELECT Name, Age FROM Sample.Person WHERE Age > :min_age ORDER BY Age, Name")
with Session(engine) as session:
    result = session.execute(stmt, {"min_age", 50})
    for row in result:
        print(f"Name: {row.Name}\nAge: {row.Age}\n")

SQLAlchemy Core: Fine-Grained SQL Control

SQLAlchemy Core provides a schema-first, lower-level approach for building and executing SQL statements using Python constructs. It gives you precise control over SQL generation and execution making it ideal for:

  • Complex or dynamic SQL queries.

  • Performance-sensitive applications.

  • Developers who prefer SQL-like control without raw strings.

Defining Tables with SQL Alchemy Core

You can define tables using the Table() and Column() constructors and then execute SQL statements using the select(), insert(), update(), and delete() functions. Remember to first create the table metadata. This allows SQLAlchemy to keep track of the table structure and determine whether it needs to create the table in the database when calling create_all() on the metadata object.

from sqlalchemy import Column, MetaData, Table
from sqlalchemy.sql.sqltypes import Integer, VARCHAR

# Create a table metadata
metadata = MetaData()

# Define the table structure
demo_table = Table(
    "demo_table",
    metadata,
    Column("id", Integer, primary_key = True, autoincrement = True),
    Column("value", VARCHAR(50)),
)

# Insert sample data
with engine.connect() as conn:
    conn.execute(
        demo_table.insert(),
        [
            {"id": 1, "value": "Test"},
            {"id": 2, "value": "More"},
        ],
    )
    conn.commit()
    result = conn.execute(demo_table.select()).fetchall()
    print("result:", result)
Note:

Tables created using this method will be seen as SQL.User.<table_name> under the InterSystems IRIS Management Portal.

SQLAlchemy ORM: Object-Oriented Data Access

The ORM layer abstracts away the relational structure of your data allows you to define tables as Python classes. In this way, you work with data in a more “Pythonic” way as you and interact with data using traditional Python objects.

It is ideal for:

  • Create, read, update, delete (CRUD) operations.

  • Business logic encapsulation.

  • Applications that benefit from abstraction.

Creating ORM Models

ORM allows you to define database tables as Python classes. Each class represents a table, and each attribute represents a column. This abstraction simplifies database operations and integrates well with Python applications. All tables inherit from the Base class (which itself inherits from DeclarativeBase).

from typing import List, Optional
from sqlalchemy import String, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship

class Base(DeclarativeBase):
    pass

class User(Base):
    __table__name = "user_account"

    # 'id' column is the primary key of the table    
    id: Mapped[int] = mapped_column(primary_keys=True)
    
    # 'name' column has a maximum string length of 30 characters
    name: Mapped[str] = mapped_column(String(30))
    
    # 'fullname' column is optional, so it can be NULL in the database
    fullname: Mapped[Optional[str]]

    # String representation of the object; useful for debugging
    def __repr__(self) ->:
        return f"User(id = {self.id!r}, name = {self.name!r}, fullname = {self.fullname!r})"

The general format for defining an object is:

columnName: Mapped[type] = mapped_column(db.Type, arguments)

Saving Data

To save data using ORM, create an instance of the model class and add it to a session. Then commit the session to persist the changes to the database.

Base.metadata.create_all(engine)	# Create the table if it does not exist

with Session(engine) as session:
    # Create new instances of the User class
    frodo = User(
        name = "frodo",
        fullname = "Frodo Baggins"
    )
    samwise = User(
        name = "samwise",
        fullname = "Samwise Gamgee"
    )
    pippin = User(
        name = "pippin", 
        fullname = "Pippin Took")

    # Add all three new User objects to the session (prepares them to be inserted)
    session.add_all([frodo, samwise, pippin])
    
    # Commit the session; writes the changes (inserts) to the database
    session.commit()

Retrieving Data

To retrieve data using ORM, use the Session object to query the model class. You can use filters and other query methods to refine your search results.

from sqlalchemy import select 	# 'select' is used to build SQL SELECT queries

with Session(engine) as session:
    # Print users with specific names
    stmt = select(User).where(User.name.in_(["frodo", "samwise"]))
    
    # 'scalars()' extracts the actual User objects from the result
    for user in session.scalars(stmt):
        print(user)

Deleting Data

To delete records using SQLAlchemy ORM, you first query the object you want to remove, then pass it to the session’s delete() method. Finally, commit the session to apply the change to the database.

with Session(engine) as session:
    # List of IDs to delete
    ids_to_delete = [1, 2, 3]

    for user_id in ids_to_delete:
        user = session.get(User, user_id)
        if user:
            session.delete(user)
    session.commit()
FeedbackOpens in a new tab