Hone logo
Hone
Problems

Build Your Own Python ORM

Imagine you're working on a project where you need to interact with a database, but you want a more Pythonic and object-oriented way to do it. This challenge asks you to build a simplified Object-Relational Mapper (ORM) in Python. An ORM allows you to map database tables to Python classes and database rows to Python objects, making database operations feel more like working with standard Python data structures.

Problem Description

Your task is to create a basic ORM framework that can handle simple CRUD (Create, Read, Update, Delete) operations for a single table. The ORM should abstract away the direct SQL query execution, providing a clean Python interface for interacting with the database.

Key Requirements:

  1. Model Definition: You need to define a way to declare Python classes that represent database tables. These classes should have attributes that correspond to table columns.
  2. Database Connection: The ORM should manage a connection to a database (for this challenge, we'll use an in-memory SQLite database for simplicity, but the design should be extensible).
  3. Table Creation: The ORM should be able to create the corresponding database table based on the model definition if it doesn't already exist.
  4. CRUD Operations:
    • Create (Insert): Ability to create new records in the table from Python object instances.
    • Read (Select): Ability to fetch records from the table, potentially with filtering.
    • Update: Ability to update existing records in the table.
    • Delete: Ability to delete records from the table.
  5. Instance Management: When reading data, each row should be represented as an instance of the corresponding Python model class.

Expected Behavior:

  • A user should be able to define a model class (e.g., User).
  • The ORM should automatically handle the creation of a users table with appropriate columns (e.g., id as primary key, name, email).
  • Users should be able to instantiate User objects, populate their attributes, and save them to the database.
  • Users should be able to query the database to retrieve User objects based on certain criteria.
  • Users should be able to modify existing User objects and persist those changes.
  • Users should be able to delete User objects from the database.

Edge Cases to Consider:

  • Handling of primary keys (auto-incrementing).
  • Basic data type mapping (e.g., Python str to SQL TEXT, Python int to SQL INTEGER).
  • What happens if you try to save an object before the table is created? (Your ORM should handle this gracefully, perhaps by creating the table first).

Examples

Example 1: Basic CRUD Operations

import sqlite3
from your_orm_module import ORM, Model, IntegerField, TextField

# Define a model
class User(Model):
    id = IntegerField(primary_key=True, auto_increment=True)
    name = TextField()
    email = TextField()

# Initialize ORM with an in-memory SQLite database
db_connection = sqlite3.connect(":memory:")
orm = ORM(db_connection)

# Ensure the table is created
orm.create_table(User)

# Create new users
user1 = User(name="Alice", email="alice@example.com")
user2 = User(name="Bob", email="bob@example.com")

orm.save(user1)
orm.save(user2)

# Read users
all_users = orm.get_all(User)
print(f"All users: {all_users}")
# Expected Output (IDs might vary due to auto-increment):
# All users: [<User: id=1, name='Alice', email='alice@example.com'>, <User: id=2, name='Bob', email='bob@example.com'>]

alice = orm.get_by_id(User, 1)
print(f"User with ID 1: {alice}")
# Expected Output:
# User with ID 1: <User: id=1, name='Alice', email='alice@example.com'>

bob_by_email = orm.get_one(User, User.email == "bob@example.com")
print(f"Bob by email: {bob_by_email}")
# Expected Output:
# Bob by email: <User: id=2, name='Bob', email='bob@example.com'>

# Update a user
if alice:
    alice.email = "alice.updated@example.com"
    orm.save(alice)

updated_alice = orm.get_by_id(User, 1)
print(f"Updated Alice: {updated_alice}")
# Expected Output:
# Updated Alice: <User: id=1, name='Alice', email='alice.updated@example.com'>

# Delete a user
bob_to_delete = orm.get_by_id(User, 2)
if bob_to_delete:
    orm.delete(bob_to_delete)

remaining_users = orm.get_all(User)
print(f"Users after deletion: {remaining_users}")
# Expected Output:
# Users after deletion: [<User: id=1, name='Alice', email='alice.updated@example.com'>]

Example 2: Filtering with a condition

# ... (assuming ORM and User model are defined as in Example 1 and some data is inserted)

# Get users whose name starts with 'A'
a_users = orm.get_all(User, User.name.startswith("A"))
print(f"Users starting with 'A': {a_users}")
# Expected Output:
# Users starting with 'A': [<User: id=1, name='Alice', email='alice.updated@example.com'>]

Constraints

  • The ORM should be implemented purely in Python.
  • You will be working with sqlite3 module for database operations.
  • Your ORM should support basic data types: int and str. You can map int to INTEGER and str to TEXT in SQLite.
  • Your ORM should handle a single table at a time.
  • The primary key should be an auto-incrementing integer if auto_increment=True is specified.
  • Your solution should be reasonably efficient for small to medium datasets. Direct SQL string concatenation should be avoided where possible for security and maintainability.

Notes

  • Consider how you will represent database fields in your Python model classes. You might want to create custom field types (e.g., IntegerField, TextField).
  • Think about how to automatically generate SQL CREATE TABLE statements from your model definitions.
  • For filtering, you'll need a way to represent comparison operations (e.g., equals, starts with) in a Pythonic way, which can then be translated to SQL WHERE clauses.
  • The ORM class will likely be the central orchestrator, holding the database connection and providing methods for CRUD operations.
  • The Model class will serve as a base class for your database models.
  • For simplicity, you can assume that the model class name (e.g., User) will map to a table name (e.g., users) by adding an 's' to the end.
  • The save method should handle both creating new records and updating existing ones. You can check if an object has a primary key already assigned to determine this.
Loading editor...
python