-
Notifications
You must be signed in to change notification settings - Fork 3
Schema lookup implementation #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,4 +37,5 @@ ENV/ | |
| # Misc | ||
| .DS_Store | ||
|
|
||
| __sql__ | ||
| __sql__ | ||
| fixtures | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,7 +10,8 @@ | |
| from sqlalchemy import create_engine, text | ||
| from sqlalchemy.engine import Engine | ||
| from sqlalchemy.exc import SQLAlchemyError | ||
|
|
||
| from sqlalchemy import MetaData | ||
| from sqlalchemy.schema import CreateTable | ||
| from jinja2sql import Jinja2SQL | ||
| from datetime import datetime | ||
|
|
||
|
|
@@ -232,7 +233,28 @@ def is_empty(self) -> bool: | |
| True if no rows, False otherwise | ||
| """ | ||
| return len(self.rows) == 0 | ||
|
|
||
|
|
||
| # Function to load the schema from the database | ||
| def extract_schema_from_db(db_url: str) -> str: | ||
| """Extract the schema from the database. | ||
|
|
||
| Args: | ||
| db_url: Database URL to use | ||
|
|
||
| Returns: | ||
| Schema as a string | ||
| """ | ||
| engine = create_engine(db_url) | ||
| metadata = MetaData() | ||
| metadata.reflect(bind=engine) | ||
|
|
||
| schema_lines = [] | ||
| for table in metadata.sorted_tables: | ||
| ddl = str(CreateTable(table).compile(engine)) | ||
| schema_lines.append(ddl + ";") | ||
|
|
||
| return "\n\n".join(schema_lines) | ||
|
|
||
|
|
||
| def get_db(db_url: str) -> Database: | ||
|
|
@@ -280,7 +302,8 @@ def parse_query_to_pydantic(data: Dict[str, Any], model_class: Type[BaseModel]) | |
|
|
||
| # Check the response type and transform accordingly | ||
| if model_class == int: | ||
| return int(unflattened_data["result"]) | ||
| # FIX : STILL ONLY GETS FIRST LINE OF RESPONSE | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can be a bit more defensive here i.e. do next / iter only if it is a list / iterator. |
||
| return int(next(iter(unflattened_data.values()))) | ||
| elif model_class == NoneType: | ||
| return None | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -57,15 +57,21 @@ def __init__( | |
| self.name = name | ||
| self.regen = regen | ||
| self.cache_dir = cache_dir | ||
| self.schema = schema or self.load_file(schema_path) | ||
| self.db_url = db_url or os.environ.get("DATABASE_URL") | ||
| if ( not self.db_url): | ||
| raise ValueError(f"Database URL not provided either through constructor or DATABASE_URL environment variable") | ||
|
|
||
| if (not schema and not schema_path): | ||
| # Load the schema from the database | ||
| self.schema = db.extract_schema_from_db(self.db_url) | ||
|
|
||
| else: | ||
| self.schema = schema or self.load_file(schema_path) | ||
|
|
||
| if system_prompt or system_prompt_path: | ||
| self.system_prompt = system_prompt or self.load_file(system_prompt_path) | ||
| else: | ||
| self.system_prompt = DEFAULT_SYSTEM_PROMPT | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why this removal? |
||
| self.db_url = db_url | ||
| if not self.db_url: | ||
| raise ValueError(f"Database URL not provided either through constructor or {db_url_env} environment variable") | ||
|
|
||
| # Initialize cache and SQL generator | ||
| self.cache = SQLTemplateCache(cache_dir=cache_dir) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,33 +2,39 @@ | |
| import os | ||
| from foundation_sql import db | ||
| from foundation_sql.query import SQLQueryDecorator | ||
| from typing import Optional | ||
|
|
||
| from dotenv import load_dotenv | ||
| load_dotenv() | ||
|
|
||
| DB_URL = os.environ.get("DATABSE_URL", "sqlite:///:memory:") | ||
| # DB_URL = os.environ.get("DATABASE_URL", "sqlite:///:memory:") | ||
|
|
||
| def create_query(schema): | ||
| def create_query (schema: Optional[str] = None, db_url : Optional[str] = None): | ||
| final_db_url = ( | ||
| db_url or | ||
| os.environ.get("DATABASE_URL") or | ||
| "sqlite:///:memory:" | ||
| ) | ||
| return SQLQueryDecorator(schema=schema, | ||
| db_url=DB_URL, | ||
| db_url=final_db_url, | ||
| api_key=os.getenv("OPENAI_API_KEY"), | ||
| base_url=os.getenv("OPENAI_API_BASE_URL"), | ||
| model=os.getenv("OPENAI_MODEL")) | ||
|
|
||
| class DatabaseTests(unittest.TestCase): | ||
| """Base test class for database-driven tests with common setup and helper methods.""" | ||
|
|
||
| db_url = DB_URL | ||
| db_url = "sqlite:///:memory:" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the hardcoding? |
||
| schema_sql = None | ||
| schema_path = None | ||
|
|
||
| def setUp(self): | ||
| """Create a fresh database connection for each test.""" | ||
| # Re-initialize the schema for each test to ensure clean state | ||
| #Re-initialize the schema for each test to ensure clean state | ||
| if (self.schema_sql or self.schema_path) and self.db_url: | ||
| db.get_db(self.db_url).init_schema(schema_sql=self.schema_sql, schema_path=self.schema_path) | ||
| else: | ||
| raise ValueError("At least one of schema_sql, schema_path must be provided along with db_url") | ||
| pass | ||
|
|
||
|
|
||
| def tearDown(self): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| from typing import List | ||
| from tests import common | ||
| from pydantic import BaseModel | ||
| from tests.utils import BIKES_DB_PATH, create_bike_db | ||
|
|
||
| class Bike(BaseModel): | ||
| make: str | ||
| model: str | ||
| price: int | ||
|
|
||
|
|
||
| create_bike_db() | ||
|
|
||
| query = common.create_query(db_url=f"sqlite:///{BIKES_DB_PATH}") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be better to give schema discovery as an option i.e. set it as following parameters schema=None, schema_file=None, schema_inspect=False If schema_inspect is true, none of the other two should be passed or it raises an error. Otherwise, one of schema_file or schema string should be passed. Gives more flexibility to the developer (user) of the library that way. |
||
|
|
||
| @query | ||
| def get_bikes() -> List[Bike]: | ||
| """ | ||
| Gets all bikes. | ||
| """ | ||
| pass | ||
|
|
||
| @query | ||
| def create_bike(bike: Bike) -> Bike: | ||
| """ | ||
| Creates a new bike. | ||
| """ | ||
| pass | ||
|
|
||
| @query | ||
| def get_total_price() -> int: | ||
| """ | ||
| Get the total price of all the bikes | ||
| """ | ||
| pass | ||
|
|
||
|
|
||
| class TestSchemaDiscovery(common.DatabaseTests): | ||
| db_url = f"sqlite:///{BIKES_DB_PATH}" | ||
| schema_sql = None | ||
|
|
||
| def test_schema_discovery(self): | ||
|
|
||
| re_bike = Bike(make="RE", model="Classic", price=600) | ||
| create_bike(bike=re_bike) | ||
|
|
||
| harley_bike = Bike(make="Harley", model="A very good one", price=500) | ||
| create_bike(bike = harley_bike) | ||
|
|
||
| bikes = get_bikes() | ||
| self.assertEqual(len(bikes), 2) | ||
|
|
||
| price = get_total_price() | ||
| self.assertEqual(price,1100) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably not needed. It is test specific and can be in test_schema_discovery. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # utils.py | ||
| import os | ||
| import sqlite3 | ||
|
|
||
| BIKES_DB_PATH = os.path.abspath( | ||
| os.path.join(os.path.dirname(__file__), "fixtures", "bikes.db") | ||
| ) | ||
|
|
||
| def create_bike_db(): | ||
| os.makedirs(os.path.dirname(BIKES_DB_PATH), exist_ok=True) | ||
|
|
||
|
|
||
| if os.path.exists(BIKES_DB_PATH): | ||
| os.remove(BIKES_DB_PATH) | ||
|
|
||
| conn = sqlite3.connect(BIKES_DB_PATH) | ||
| cursor = conn.cursor() | ||
| cursor.execute(""" | ||
| CREATE TABLE bikes ( | ||
| make TEXT NOT NULL, | ||
| model TEXT NOT NULL, | ||
| price INTEGER NOT NULL | ||
| ); | ||
| """) | ||
| conn.commit() | ||
| conn.close() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this addition of fixtures?