oraffi 1.0.0 copy "oraffi: ^1.0.0" to clipboard
oraffi: ^1.0.0 copied to clipboard

High-performance Oracle Database driver for Dart using FFI. Direct Oracle access via ODPI-C with connection pooling, transactions, stored procedures, REF CURSOR, LOBs, and batch operations.

Oraffi - Oracle Database Driver for Dart #

A high-performance Oracle database driver for Dart, using FFI to interface with Oracle's ODPI-C library. Provides a clean, type-safe API with support for connection pooling, transactions, stored procedures, LOBs, and more.

Features #

  • High Performance - Direct FFI bindings to Oracle ODPI-C
  • Connection Pooling - Built-in pool with configurable min/max connections
  • Prepared Statements - Parameterized queries to prevent SQL injection
  • Transaction Management - Full commit/rollback control with isolation levels
  • Stored Procedures - Call procedures with IN/OUT/INOUT parameters
  • REF CURSOR - Return result sets from stored procedures
  • LOB Support - CLOB, BLOB, NCLOB read/write operations
  • Batch Operations - executemany() for bulk inserts (10-100x faster)
  • UTF-8 Support - Full international character and emoji support
  • Type Safety - Strong typing with comprehensive error handling

Requirements #

This package requires two native libraries:

  1. Oracle Instant Client - Oracle's client libraries (from Oracle)
  2. ODPI-C - Oracle Database Programming Interface for C (build from source)

Step 1: Oracle Instant Client #

Download from Oracle (free registration required): https://www.oracle.com/database/technologies/instant-client/downloads.html

macOS

# Download "Basic Package" DMG from Oracle for your architecture (Intel or ARM64)
# Mount and copy to /usr/local/oracle:
sudo mkdir -p /usr/local/oracle
sudo cp -r /Volumes/instantclient-basic-macos.arm64-*/. /usr/local/oracle/instantclient

# Set library path (add to ~/.zshrc or ~/.bashrc)
export DYLD_LIBRARY_PATH="/usr/local/oracle/instantclient:$DYLD_LIBRARY_PATH"

Linux

# Install libaio (required dependency)
sudo apt-get install libaio1  # Debian/Ubuntu
sudo yum install libaio       # RHEL/CentOS

# Download and extract Basic Package
mkdir -p ~/oracle
cd ~/oracle
unzip instantclient-basic-linux.x64-*.zip

# Set library path (add to ~/.bashrc)
export LD_LIBRARY_PATH="$HOME/oracle/instantclient_23_5:$LD_LIBRARY_PATH"

Windows

# Download and extract Basic Package to C:\oracle\instantclient
# Add to System PATH: C:\oracle\instantclient

Step 2: ODPI-C Library #

ODPI-C must be built from source (Oracle does not provide pre-built binaries).

macOS / Linux

# Clone and build ODPI-C
git clone https://github.com/oracle/odpi.git
cd odpi
make

# Install (copies libodpic to /usr/local/lib)
sudo make install

# On Linux, update library cache
sudo ldconfig

Windows

# Requires Visual Studio Build Tools
git clone https://github.com/oracle/odpi.git
cd odpi
nmake -f Makefile.win32

# Copy odpic.dll to your PATH or application directory

Verify Installation #

# Check ODPI-C is installed
ls -la /usr/local/lib/libodpic*

# Check Oracle Instant Client
ls -la /usr/local/oracle/instantclient/libclntsh*

Installation #

Add to your pubspec.yaml:

dependencies:
  oraffi: ^1.0.0
dart pub get

Quick Start #

import 'package:oraffi/oraffi.dart';

Future<void> main() async {
  final db = OracleDb();

  // Initialize Oracle client (auto-finds ODPI-C library)
  await db.initOracleClient();

  // OR specify ODPI-C library path explicitly
  // await db.initOracleClient(libraryPath: '/usr/local/lib/libodpic.dylib');

  // Connect with direct parameters
  final conn = await db.createConnection(
    config: OracleConfig.create(
      username: 'myuser',
      password: 'mypassword',
      connectionString: 'localhost:1521/XEPDB1',
    ),
  );

  try {
    // Query data
    final results = await conn.query('SELECT * FROM employees WHERE department_id = :1', params: [10]);

    for (final row in results) {
      print('${row['FIRST_NAME']} ${row['LAST_NAME']}');
    }
  } finally {
    await conn.close();
  }

  await db.shutdown();
}
# Set environment variables
export ORACLE_USER="myuser"
export ORACLE_PASSWORD="mypassword"
export ORACLE_CONNECTION_STRING="localhost:1521/XEPDB1"
import 'package:oraffi/oraffi.dart';

Future<void> main() async {
  final db = OracleDb();
  await db.initOracleClient();

  // Load config from environment
  final conn = await db.createConnection(
    config: OracleConfig.fromEnvironment(),
  );

  try {
    final results = await conn.query('SELECT SYSDATE FROM DUAL');
    print('Server time: ${results.first['SYSDATE']}');
  } finally {
    await conn.close();
  }

  await db.shutdown();
}

Connection Configuration #

OracleConfig Options #

final config = OracleConfig.create(
  username: 'myuser',
  password: 'mypassword',
  connectionString: 'host:port/service_name',  // Simple format
  // OR
  // connectionString: '//host:port/service_name',  // Easy Connect
  // OR
  // connectionString: 'mydb',  // TNS alias (requires tnsnames.ora)
);

Connection String Formats #

Format Example
Simple localhost:1521/XEPDB1
Easy Connect //localhost:1521/XEPDB1
TNS Alias MYDB (requires tnsnames.ora)
Full TNS (DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=...)))

Connection Pooling #

final db = OracleDb();
await db.initOracleClient();

// Create a connection pool
await db.createPool(
  config: OracleConfig.create(
    username: 'myuser',
    password: 'mypassword',
    connectionString: 'localhost:1521/XEPDB1',
  ),
  poolConfig: PoolConfig(
    poolMin: 2,
    poolMax: 10,
    poolIncrement: 1,
  ),
);

// Get connection from pool
final conn = await db.getConnection();
try {
  final results = await conn.query('SELECT * FROM users');
  // Process results...
} finally {
  await conn.close();  // Returns to pool
}

// Check pool statistics
final pool = db.getPool();
final stats = await pool!.getStatistics();
print('Open: ${stats.openConnections}, Busy: ${stats.busyConnections}');

await db.shutdown();

Parameterized Queries #

Positional Parameters #

// Use :1, :2, :3 placeholders (Oracle native syntax)
final results = await conn.query(
  'SELECT * FROM employees WHERE department_id = :1 AND salary > :2',
  params: [10, 50000],
);

Named Parameters #

// Use :name placeholders
final results = await conn.query(
  'SELECT * FROM employees WHERE department_id = :dept_id AND salary > :min_salary',
  namedParams: {'dept_id': 10, 'min_salary': 50000},
);

Transactions #

try {
  await conn.executeUpdate('INSERT INTO accounts (id, balance) VALUES (:1, :2)', params: [1, 1000]);
  await conn.executeUpdate('INSERT INTO accounts (id, balance) VALUES (:1, :2)', params: [2, 2000]);

  await conn.commit();  // Commit transaction
} catch (e) {
  await conn.rollback();  // Rollback on error
  rethrow;
}

Autocommit Mode #

await conn.setAutocommit(true);  // Each statement auto-commits

await conn.executeUpdate('INSERT INTO logs (msg) VALUES (:1)', params: ['Event 1']);
// Automatically committed

await conn.setAutocommit(false);  // Back to manual commit

Transaction Isolation Levels #

await conn.setTransactionIsolation(TransactionIsolation.serializable);
// Options: readCommitted, serializable, readOnly

Stored Procedures #

Basic Call with OUT Parameters #

// Create OUT parameters
final nameOut = await conn.outString(maxSize: 100);
final salaryOut = await conn.outNumber();

await conn.callproc(
  'get_employee_info',
  parameters: [101],  // IN parameter: employee_id
  keywordParameters: {
    'emp_name': nameOut,
    'emp_salary': salaryOut,
  },
);

print('Name: ${nameOut.getValue()}');
print('Salary: ${salaryOut.getValue()}');

// Cleanup
nameOut.dispose();
salaryOut.dispose();

REF CURSOR (Returning Result Sets) #

final cursorOut = await conn.outRefCursor();

await conn.callproc(
  'get_employees_by_dept',
  parameters: [10],  // department_id
  keywordParameters: {'emp_cursor': cursorOut},
);

// Get cursor as statement
final cursor = cursorOut.getValue() as OracleStatement;

// Iterate results
await for (final row in cursor.rows()) {
  print('${row['FIRST_NAME']} ${row['LAST_NAME']}');
}

await cursor.close();
cursorOut.dispose();

Batch Operations #

Insert thousands of rows efficiently with executemany():

final data = [
  ['John', 'john@example.com', 30],
  ['Jane', 'jane@example.com', 25],
  ['Bob', 'bob@example.com', 35],
  // ... thousands more rows
];

final stmt = await conn.prepare(
  'INSERT INTO users (name, email, age) VALUES (:1, :2, :3)',
);

await stmt.executemany(data);
await conn.commit();
await stmt.close();

LOB Handling #

CLOB (Character Large Object) #

// Create and write
final clob = await conn.createLob(LobType.clob);
await clob.write('Large text content...');

// Insert into table
await conn.executeUpdate(
  'INSERT INTO documents (id, content) VALUES (:1, :2)',
  params: [1, clob],
);

// Read from table
final results = await conn.query('SELECT content FROM documents WHERE id = :1', params: [1]);
final lobValue = results.first['CONTENT'] as OracleLob;
final text = await lobValue.read();
print('Content: $text');

await clob.close();
clob.dispose();

Streaming Large Result Sets #

For memory-efficient processing of large results:

final stmt = await conn.execute('SELECT * FROM large_table');

// Set prefetch for better performance
await stmt.setPrefetchRows(1000);

// Stream rows instead of loading all into memory
int count = 0;
await for (final row in stmt.rows()) {
  count++;
  // Process row...
}
print('Processed $count rows');

await stmt.close();

Error Handling #

try {
  await conn.query('SELECT * FROM nonexistent_table');
} on OracleStatementException catch (e) {
  print('SQL Error: ${e.message}');
  print('Error Code: ${e.errorCode}');  // e.g., 942 for table not found
  print('SQL: ${e.sql}');
} on OracleConnectionException catch (e) {
  print('Connection Error: ${e.message}');
} on OracleException catch (e) {
  print('Oracle Error: ${e.message}');
}

Examples #

The example/ directory contains 13 complete, runnable examples:

Example Description
01_basic_connection.dart Connect and query
02_simple_queries.dart SELECT statements
03_parameterized_queries.dart Bind parameters
04_batch_operations.dart Bulk inserts with executemany
05_stored_procedures.dart Calling PL/SQL
06_lob_handling.dart CLOB/BLOB operations
07_connection_pooling.dart Pool management
08_transactions.dart Commit/rollback
09_streaming_large_results.dart Memory-efficient streaming
10_utf8_international.dart Unicode support
11_named_parameters.dart Named bind variables
12_ref_cursor.dart REF CURSOR from procedures
13_autocommit_and_isolation.dart Transaction modes

Run any example:

export ORACLE_USER="system"
export ORACLE_PASSWORD="MyPassword123"
export ORACLE_CONNECTION_STRING="localhost:1521/XEPDB1"
export DYLD_LIBRARY_PATH="/path/to/instantclient:$DYLD_LIBRARY_PATH"

dart run example/01_basic_connection.dart

Testing with Docker #

Quick Oracle database for development:

# Start Oracle XE
docker run -d --name oracle-xe -p 1521:1521 \
  -e ORACLE_PWD=MyPassword123 \
  gvenzl/oracle-xe:21-slim

# Wait for startup (takes 1-2 minutes)
docker logs -f oracle-xe

# Set connection credentials
export ORACLE_USER="system"
export ORACLE_PASSWORD="MyPassword123"
export ORACLE_CONNECTION_STRING="localhost:1521/XEPDB1"

# Ensure native libraries are accessible (see Requirements section)
# macOS:
export DYLD_LIBRARY_PATH="/usr/local/oracle/instantclient:$DYLD_LIBRARY_PATH"
# Linux:
export LD_LIBRARY_PATH="$HOME/oracle/instantclient_23_5:$LD_LIBRARY_PATH"

# Run an example
dart run example/01_basic_connection.dart

Library Configuration #

ODPI-C Library Path #

ODPI-C can be specified in three ways (checked in order):

// 1. Direct parameter (highest priority)
await db.initOracleClient(libraryPath: '/path/to/libodpic.dylib');

// 2. Environment variable
// export ORACLE_CLIENT_LIB_PATH="/path/to/libodpic.dylib"

// 3. Auto-search (checks /usr/local/lib, /usr/lib, current directory, etc.)
await db.initOracleClient();

Oracle Instant Client Path #

Oracle Instant Client must be in the system library path:

# macOS
export DYLD_LIBRARY_PATH="/path/to/instantclient:$DYLD_LIBRARY_PATH"

# Linux
export LD_LIBRARY_PATH="/path/to/instantclient:$LD_LIBRARY_PATH"

# Windows - add to PATH environment variable

Note: Oracle Instant Client path cannot be set in Dart code. ODPI-C uses OS-level dynamic loading (dlopen), which reads from system library paths. This is standard for all Oracle database drivers.

Troubleshooting #

"Cannot locate Oracle Client library" #

Ensure both libraries are accessible:

# Check ODPI-C
ls -la /usr/local/lib/libodpic*

# Check Oracle Instant Client (macOS)
ls -la $DYLD_LIBRARY_PATH/libclntsh*

# Check Oracle Instant Client (Linux)
ls -la $LD_LIBRARY_PATH/libclntsh*

"ORA-12154: TNS could not resolve" #

Check connection string format:

// Correct
connectionString: 'localhost:1521/XEPDB1'

// Wrong
connectionString: 'localhost/XEPDB1'  // Missing port

"ORA-01017: invalid username/password" #

Verify credentials:

print('User: ${config.username}');
print('Connection: ${config.connectionString}');

API Reference #

Core Classes #

Class Description
OracleDb Main entry point, manages pools and client
OracleConnection Database connection with query methods
OracleConnectionPool Connection pool with statistics
OracleStatement Prepared statement for queries
OracleVariable Bind variable for OUT parameters
OracleLob LOB (CLOB/BLOB) handling
OracleConfig Connection configuration
PoolConfig Pool configuration

Key Methods #

OracleConnection:

  • query(sql, {params, namedParams}) - Execute and return all rows
  • execute(sql, {params, namedParams}) - Execute and return statement for streaming
  • executeUpdate(sql, {params, namedParams}) - Execute DML, return rows affected
  • prepare(sql) - Prepare statement for batch execution
  • callproc(name, {parameters, keywordParameters}) - Call stored procedure
  • commit() / rollback() - Transaction control
  • ping() - Check connection health
  • close() - Close connection

OracleStatement:

  • rows() - Stream result rows
  • fetchOne() - Fetch single row
  • fetchAll() - Fetch all rows
  • executemany(data) - Batch execute
  • close() - Close statement

License #

MIT License - See LICENSE file for details.

Contributing #

Contributions welcome! Please read the contributing guidelines and submit pull requests.

1
likes
120
points
134
downloads

Publisher

unverified uploader

Weekly Downloads

High-performance Oracle Database driver for Dart using FFI. Direct Oracle access via ODPI-C with connection pooling, transactions, stored procedures, REF CURSOR, LOBs, and batch operations.

Repository (GitHub)
View/report issues
Contributing

Topics

#database #oracle #sql #ffi

Documentation

API reference

License

MIT (license)

Dependencies

ffi, path

More

Packages that depend on oraffi