From 02c7cbd1cda9ced5725cb16f9147b8d7a4063bea Mon Sep 17 00:00:00 2001 From: pacnpal <183241239+pacnpal@users.noreply.github.com> Date: Sat, 23 Aug 2025 18:42:09 -0400 Subject: [PATCH] Implement code changes to enhance functionality and improve performance --- context_portal/alembic.ini | 43 ++++ context_portal/alembic/env.py | 79 +++++++ .../versions/2025_06_17_initial_schema.py | 195 ++++++++++++++++++ context_portal/context.db | Bin 0 -> 114688 bytes 4 files changed, 317 insertions(+) create mode 100644 context_portal/alembic.ini create mode 100644 context_portal/alembic/env.py create mode 100644 context_portal/alembic/versions/2025_06_17_initial_schema.py create mode 100644 context_portal/context.db diff --git a/context_portal/alembic.ini b/context_portal/alembic.ini new file mode 100644 index 00000000..e06ccc49 --- /dev/null +++ b/context_portal/alembic.ini @@ -0,0 +1,43 @@ + +# A generic Alembic configuration file. + +[alembic] +# path to migration scripts +script_location = alembic + +# The database URL is now set dynamically by ConPort's run_migrations function. +# sqlalchemy.url = sqlite:///your_database.db +# ... other Alembic settings ... +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/context_portal/alembic/env.py b/context_portal/alembic/env.py new file mode 100644 index 00000000..105ea305 --- /dev/null +++ b/context_portal/alembic/env.py @@ -0,0 +1,79 @@ + +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line prevents the need to have a separate logging config file. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = None + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/context_portal/alembic/versions/2025_06_17_initial_schema.py b/context_portal/alembic/versions/2025_06_17_initial_schema.py new file mode 100644 index 00000000..d642ff55 --- /dev/null +++ b/context_portal/alembic/versions/2025_06_17_initial_schema.py @@ -0,0 +1,195 @@ + +"""Initial schema + +Revision ID: 20250617 +Revises: +Create Date: 2025-06-17 15:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +import json + +# revision identifiers, used by Alembic. +revision = '20250617' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto-generated by Alembic - please adjust! ### + op.create_table('active_context', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('active_context_history', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('timestamp', sa.DateTime(), nullable=False), + sa.Column('version', sa.Integer(), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('change_source', sa.String(length=255), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('context_links', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('workspace_id', sa.String(length=1024), nullable=False), + sa.Column('source_item_type', sa.String(length=255), nullable=False), + sa.Column('source_item_id', sa.String(length=255), nullable=False), + sa.Column('target_item_type', sa.String(length=255), nullable=False), + sa.Column('target_item_id', sa.String(length=255), nullable=False), + sa.Column('relationship_type', sa.String(length=255), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('timestamp', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_context_links_source_item_id'), 'context_links', ['source_item_id'], unique=False) + op.create_index(op.f('ix_context_links_source_item_type'), 'context_links', ['source_item_type'], unique=False) + op.create_index(op.f('ix_context_links_target_item_id'), 'context_links', ['target_item_id'], unique=False) + op.create_index(op.f('ix_context_links_target_item_type'), 'context_links', ['target_item_type'], unique=False) + op.create_table('custom_data', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('timestamp', sa.DateTime(), nullable=False), + sa.Column('category', sa.String(length=255), nullable=False), + sa.Column('key', sa.String(length=255), nullable=False), + sa.Column('value', sa.Text(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('category', 'key') + ) + op.create_table('decisions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('timestamp', sa.DateTime(), nullable=False), + sa.Column('summary', sa.Text(), nullable=False), + sa.Column('rationale', sa.Text(), nullable=True), + sa.Column('implementation_details', sa.Text(), nullable=True), + sa.Column('tags', sa.Text(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('product_context', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('product_context_history', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('timestamp', sa.DateTime(), nullable=False), + sa.Column('version', sa.Integer(), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('change_source', sa.String(length=255), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('progress_entries', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('timestamp', sa.DateTime(), nullable=False), + sa.Column('status', sa.String(length=50), nullable=False), + sa.Column('description', sa.Text(), nullable=False), + sa.Column('parent_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['parent_id'], ['progress_entries.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('system_patterns', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('timestamp', sa.DateTime(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('tags', sa.Text(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + + # Seed initial data + op.execute("INSERT INTO product_context (id, content) VALUES (1, '{}')") + op.execute("INSERT INTO active_context (id, content) VALUES (1, '{}')") + + # Create FTS5 virtual table for decisions + op.execute(''' + CREATE VIRTUAL TABLE decisions_fts USING fts5( + summary, + rationale, + implementation_details, + tags, + content="decisions", + content_rowid="id" + ); + ''') + + # Create triggers to keep the FTS table in sync with the decisions table + op.execute(''' + CREATE TRIGGER decisions_after_insert AFTER INSERT ON decisions + BEGIN + INSERT INTO decisions_fts (rowid, summary, rationale, implementation_details, tags) + VALUES (new.id, new.summary, new.rationale, new.implementation_details, new.tags); + END; + ''') + op.execute(''' + CREATE TRIGGER decisions_after_delete AFTER DELETE ON decisions + BEGIN + INSERT INTO decisions_fts (decisions_fts, rowid, summary, rationale, implementation_details, tags) + VALUES ('delete', old.id, old.summary, old.rationale, old.implementation_details, old.tags); + END; + ''') + op.execute(''' + CREATE TRIGGER decisions_after_update AFTER UPDATE ON decisions + BEGIN + INSERT INTO decisions_fts (decisions_fts, rowid, summary, rationale, implementation_details, tags) + VALUES ('delete', old.id, old.summary, old.rationale, old.implementation_details, old.tags); + INSERT INTO decisions_fts (rowid, summary, rationale, implementation_details, tags) + VALUES (new.id, new.summary, new.rationale, new.implementation_details, new.tags); + END; + ''') + + # Create FTS5 virtual table for custom_data + op.execute(''' + CREATE VIRTUAL TABLE custom_data_fts USING fts5( + category, + key, + value_text, + content="custom_data", + content_rowid="id" + ); + ''') + + # Create triggers for custom_data_fts + op.execute(''' + CREATE TRIGGER custom_data_after_insert AFTER INSERT ON custom_data + BEGIN + INSERT INTO custom_data_fts (rowid, category, key, value_text) + VALUES (new.id, new.category, new.key, new.value); + END; + ''') + op.execute(''' + CREATE TRIGGER custom_data_after_delete AFTER DELETE ON custom_data + BEGIN + INSERT INTO custom_data_fts (custom_data_fts, rowid, category, key, value_text) + VALUES ('delete', old.id, old.category, old.key, old.value); + END; + ''') + op.execute(''' + CREATE TRIGGER custom_data_after_update AFTER UPDATE ON custom_data + BEGIN + INSERT INTO custom_data_fts (custom_data_fts, rowid, category, key, value_text) + VALUES ('delete', old.id, old.category, old.key, old.value); + INSERT INTO custom_data_fts (rowid, category, key, value_text) + VALUES (new.id, new.category, new.key, new.value); + END; + ''') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto-generated by Alembic - please adjust! ### + op.drop_table('system_patterns') + op.drop_table('progress_entries') + op.drop_table('product_context_history') + op.drop_table('product_context') + op.drop_table('decisions') + op.drop_table('custom_data') + op.drop_index(op.f('ix_context_links_target_item_type'), table_name='context_links') + op.drop_index(op.f('ix_context_links_target_item_id'), table_name='context_links') + op.drop_index(op.f('ix_context_links_source_item_type'), table_name='context_links') + op.drop_index(op.f('ix_context_links_source_item_id'), table_name='context_links') + op.drop_table('context_links') + op.drop_table('active_context_history') + op.drop_table('active_context') + # ### end Alembic commands ### diff --git a/context_portal/context.db b/context_portal/context.db new file mode 100644 index 0000000000000000000000000000000000000000..814406ab8611a22727c365e3fdfcdc64819d96af GIT binary patch literal 114688 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 G|Np=Lx-ruL literal 0 HcmV?d00001