From d8ac3323f6f93c4324eda8d8fc3bf0b90d5f0163 Mon Sep 17 00:00:00 2001 From: Oblio Date: Wed, 25 Mar 2026 00:38:53 -0400 Subject: [PATCH 1/2] marker: Repository updated 2026-03-25 04:40 UTC Confirming clawbot-sql-memory is current and deployable. sql-memory v2.0 with pymssql transport, UTC timestamps, parameterised queries. Compatible with sql-connector v2.1 (dynamic backend config via .env) --- UPDATE_MARKER.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 UPDATE_MARKER.txt diff --git a/UPDATE_MARKER.txt b/UPDATE_MARKER.txt new file mode 100644 index 0000000..9fbaaa4 --- /dev/null +++ b/UPDATE_MARKER.txt @@ -0,0 +1 @@ +# Updated Wed Mar 25 00:38:53 EDT 2026 From 181acabce50897507cabc76e39d234e5334956e5 Mon Sep 17 00:00:00 2001 From: Oblio Date: Wed, 25 Mar 2026 00:42:08 -0400 Subject: [PATCH 2/2] fix: Update README to reflect GUID schema + remove stale files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SCHEMA UPDATES: - All INT IDENTITY → UNIQUEIDENTIFIER PRIMARY KEY DEFAULT newid() - Tables affected: Memories, TaskQueue, ActivityLog, Sessions, KnowledgeIndex, Todos ENV CONFIGURATION: - Updated .env docs to use dynamic backend naming pattern (SQL__*) - Pattern: SQL_local_*, SQL_cloud_*, or any custom backend - Added reference to sql-connector README for details CLEANUP: - Removed UPDATE_MARKER.txt (temporary marker file) - Deleted embedded sql-connector/ directory (not needed, use separate repo) COMPATIBILITY: - Still imports from clawbot-sql-connector v2.1 (with dynamic backend config) - All APIs unchanged - UTC timestamps, parameterised queries throughout Ready for main branch merge. --- README.md | 38 ++--- UPDATE_MARKER.txt | 1 - sql-connector/SKILL.md | 63 -------- sql-connector/sql_connector.py | 253 --------------------------------- 4 files changed, 22 insertions(+), 333 deletions(-) delete mode 100644 UPDATE_MARKER.txt delete mode 100644 sql-connector/SKILL.md delete mode 100644 sql-connector/sql_connector.py diff --git a/README.md b/README.md index 538cf5c..0048b1d 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ CREATE SCHEMA memory; GO CREATE TABLE memory.Memories ( - id INT IDENTITY(1,1) PRIMARY KEY, + id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT newid(), category NVARCHAR(100) NOT NULL, key NVARCHAR(255) NOT NULL, content NVARCHAR(MAX) NOT NULL, @@ -42,7 +42,7 @@ CREATE TABLE memory.Memories ( ); CREATE TABLE memory.TaskQueue ( - id INT IDENTITY(1,1) PRIMARY KEY, + id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT newid(), agent NVARCHAR(100) NOT NULL, task_type NVARCHAR(100) NOT NULL, payload NVARCHAR(MAX) DEFAULT '', @@ -58,7 +58,7 @@ CREATE TABLE memory.TaskQueue ( ); CREATE TABLE memory.ActivityLog ( - id INT IDENTITY(1,1) PRIMARY KEY, + id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT newid(), event_type NVARCHAR(100) NOT NULL, agent NVARCHAR(100) DEFAULT '', description NVARCHAR(MAX) DEFAULT '', @@ -68,7 +68,7 @@ CREATE TABLE memory.ActivityLog ( ); CREATE TABLE memory.Sessions ( - id INT IDENTITY(1,1) PRIMARY KEY, + id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT newid(), session_key NVARCHAR(255) NOT NULL, agent NVARCHAR(100) DEFAULT '', status NVARCHAR(50) DEFAULT 'active', @@ -78,7 +78,7 @@ CREATE TABLE memory.Sessions ( ); CREATE TABLE memory.KnowledgeIndex ( - id INT IDENTITY(1,1) PRIMARY KEY, + id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT newid(), domain NVARCHAR(100) NOT NULL, key NVARCHAR(255) NOT NULL, content NVARCHAR(MAX) NOT NULL, @@ -88,7 +88,7 @@ CREATE TABLE memory.KnowledgeIndex ( ); CREATE TABLE memory.Todos ( - id INT IDENTITY(1,1) PRIMARY KEY, + id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT newid(), title NVARCHAR(500) NOT NULL, description NVARCHAR(MAX) DEFAULT '', priority INT DEFAULT 3, @@ -103,22 +103,28 @@ GO ## Step 2: Configure .env +Backend configuration uses a simple naming pattern. Add these to your `.env`: + ```env # Local SQL Server -SQL_SERVER=10.0.0.110 -SQL_PORT=1433 -SQL_DATABASE=YourDatabase -SQL_USER=your_user -SQL_PASSWORD=your_password +SQL_local_server=10.0.0.110 +SQL_local_port=1433 +SQL_local_database=YourDatabase +SQL_local_user=your_user +SQL_local_password=your_password # Cloud SQL Server (Azure / site4now / etc.) -SQL_CLOUD_SERVER=yourserver.database.windows.net -SQL_CLOUD_PORT=1433 -SQL_CLOUD_DATABASE=your_cloud_db -SQL_CLOUD_USER=your_cloud_user -SQL_CLOUD_PASSWORD=your_cloud_password +SQL_cloud_server=yourserver.database.windows.net +SQL_cloud_port=1433 +SQL_cloud_database=your_cloud_db +SQL_cloud_user=your_cloud_user +SQL_cloud_password=your_cloud_password + +# Add more backends using the same pattern: SQL__server, SQL__database, etc. ``` +See [clawbot-sql-connector README](https://github.com/VeXHarbinger/clawbot-sql-connector#env-setup) for more details on backend naming. + ## Step 3: Install ```bash diff --git a/UPDATE_MARKER.txt b/UPDATE_MARKER.txt deleted file mode 100644 index 9fbaaa4..0000000 --- a/UPDATE_MARKER.txt +++ /dev/null @@ -1 +0,0 @@ -# Updated Wed Mar 25 00:38:53 EDT 2026 diff --git a/sql-connector/SKILL.md b/sql-connector/SKILL.md deleted file mode 100644 index 535fcf1..0000000 --- a/sql-connector/SKILL.md +++ /dev/null @@ -1,63 +0,0 @@ -# SQL Connector Skill -> Generic SQL Server connectivity for OpenClaw agents - -## Overview -Provides a reusable, battle-tested SQL Server connection layer with automatic retry, connection pooling, parameterized queries, and structured error handling. Built for MSSQL (via `sqlcmd`) with plans for ODBC/pyodbc support. - -## Installation -```bash -clawhub install sql-connector -``` - -## Usage - -### Quick Start -```python -from sql_connector import SQLConnector - -# From environment variables -conn = SQLConnector.from_env('cloud') # reads SQL_CLOUD_SERVER, etc. - -# Direct -conn = SQLConnector(server='myserver.com', database='mydb', user='sa', password='secret') - -# Execute queries -result = conn.execute("SELECT COUNT(*) FROM users") -scalar = conn.execute_scalar("SELECT MAX(id) FROM orders") -rows = conn.query("SELECT id, name FROM products WHERE active=1") -``` - -### Features -- **Auto-retry** with exponential backoff (configurable) -- **Connection validation** via `ping()` -- **Parameterized queries** to prevent SQL injection -- **Structured result parsing** — rows returned as list of dicts -- **Logging** — all queries logged at DEBUG level -- **Error classification** — distinguishes connection vs. query errors - -### Configuration -Environment variables follow the pattern `SQL_{PROFILE}_*`: -``` -SQL_CLOUD_SERVER=sql5112.site4now.net -SQL_CLOUD_DATABASE=db_99ba1f_memory4oblio -SQL_CLOUD_USER=myuser -SQL_CLOUD_PASSWORD=mypassword -``` - -### API Reference - -| Method | Description | -|--------|-------------| -| `execute(sql)` | Execute SQL, return raw output | -| `execute_scalar(sql)` | Execute SQL, return single value | -| `query(sql, columns)` | Execute SELECT, return list of dicts | -| `ping()` | Test connection, return bool | -| `from_env(profile)` | Create from env vars | - -## Requirements -- `sqlcmd` (mssql-tools) installed and in PATH -- Python 3.8+ -- `.env` file with SQL credentials - -## License -MIT diff --git a/sql-connector/sql_connector.py b/sql-connector/sql_connector.py deleted file mode 100644 index d0a88d2..0000000 --- a/sql-connector/sql_connector.py +++ /dev/null @@ -1,253 +0,0 @@ -#!/usr/bin/env python3 -""" -sql_connector.py — Generic SQL Server Connector (v2.0) -======================================================= -Reusable, driver-native SQL Server connectivity for OpenClaw agents. -Transport: pymssql (native TDS driver — no subprocess, no sqlcmd dependency). - -Security model: - - SQLConnector is abstract (ABC) — cannot be instantiated directly - - execute() and query() are sealed via metaclass — subclasses cannot bypass them - - All queries must use parameterised binding (%s placeholders) - - No string interpolation in execute/query — enforced by design - - Credentials loaded from environment only - -Upgrade path from v1.x (sqlcmd-based): - - API is backward-compatible: from_env(), execute(), query(), ping() all preserved - - execute() now returns bool (success/failure) instead of raw stdout string - - query() now returns list[dict] directly (no columns arg needed) - - execute_scalar() preserved, returns Any instead of Optional[str] - - New: scalar() method for single-value queries - -Usage: - from sql_connector import MSSQLConnector, get_connector - db = get_connector('cloud') - rows = db.query("SELECT id, name FROM memory.Memories WHERE category=%s", ('facts',)) - ok = db.execute("UPDATE memory.Memories SET importance=%s WHERE id=%s", (5, 42)) - val = db.scalar("SELECT COUNT(*) FROM memory.TaskQueue WHERE status=%s", ('pending',)) -""" - -from __future__ import annotations - -import abc -import logging -import os -import time -from typing import Any - -import pymssql -from dotenv import load_dotenv - -# Walk up from this file to find .env (handles install into skills/ subdir) -import pathlib as _pathlib -def _find_env() -> str | None: - p = _pathlib.Path(os.path.abspath(__file__)).parent - for _ in range(5): - c = p / '.env' - if c.exists(): - return str(c) - p = p.parent - return None - -_env = _find_env() -if _env: - load_dotenv(_env, override=True) - -_log = logging.getLogger(__name__) - -# ── Backend configuration ───────────────────────────────────────────────────── - -_BACKENDS: dict[str, dict[str, Any]] = { - 'local': { - 'server': os.getenv('SQL_SERVER', os.getenv('SQL_LOCAL_SERVER', '10.0.0.110')), - 'port': int(os.getenv('SQL_PORT', os.getenv('SQL_LOCAL_PORT', '1433'))), - 'database': os.getenv('SQL_DATABASE', os.getenv('SQL_LOCAL_DATABASE', 'Oblio_Memories')), - 'user': os.getenv('SQL_USER', os.getenv('SQL_LOCAL_USER', 'oblio')), - 'password': os.getenv('SQL_PASSWORD', os.getenv('SQL_LOCAL_PASSWORD', '')), - }, - 'cloud': { - 'server': os.getenv('SQL_CLOUD_SERVER', ''), - 'port': int(os.getenv('SQL_CLOUD_PORT', '1433')), - 'database': os.getenv('SQL_CLOUD_DATABASE', ''), - 'user': os.getenv('SQL_CLOUD_USER', ''), - 'password': os.getenv('SQL_CLOUD_PASSWORD', ''), - }, -} - - -# ── Metaclass: seal execute/query against subclass override ────────────────── - -_SEALED = frozenset({'execute', 'query'}) - -class _SealCoreMethods(abc.ABCMeta): - """Prevent any subclass from overriding execute() or query().""" - def __new__(mcs, name, bases, namespace): - for method in _SEALED: - if method in namespace: - for base in bases: - for ancestor in getattr(base, '__mro__', []): - if method in vars(ancestor) and getattr(ancestor, '__name__', '') == 'SQLConnector': - raise TypeError( - f"{name}: '{method}()' is sealed and cannot be overridden. " - "Add domain logic in a repository subclass instead." - ) - return super().__new__(mcs, name, bases, namespace) - - -# ── Custom exceptions ───────────────────────────────────────────────────────── - -class SQLConnectorError(Exception): - """Base connector error.""" - -class SQLConnectionError(SQLConnectorError): - """Connection-level failure (retry-eligible).""" - -class SQLQueryError(SQLConnectorError): - """Query execution failure (do not retry).""" - - -# ── Abstract base ───────────────────────────────────────────────────────────── - -class SQLConnector(abc.ABC, metaclass=_SealCoreMethods): - """ - Abstract SQL connector. - Concrete subclasses must implement _connect(). - execute() and query() are sealed — extend via repository subclasses. - """ - - MAX_RETRIES: int = 3 - RETRY_DELAY: float = 2.0 - - def __init__(self, backend: str = 'cloud') -> None: - if backend not in _BACKENDS: - raise ValueError(f"Unknown backend '{backend}'. Options: {list(_BACKENDS)}") - self._backend = backend - self._cfg = _BACKENDS[backend] - - @classmethod - def from_env(cls, profile: str = 'cloud', **kwargs) -> 'SQLConnector': - """Create connector from environment variables (v1.x compat).""" - if profile not in _BACKENDS: - raise SQLConnectionError(f"Unknown profile '{profile}'") - instance = cls.__new__(cls) - SQLConnector.__init__(instance, profile) - return instance - - @abc.abstractmethod - def _connect(self) -> Any: - """Return an open DB-API 2.0 connection.""" - - # ── Sealed public API ───────────────────────────────────────────────────── - - def execute(self, sql: str, params: tuple = ()) -> bool: - """ - Run INSERT / UPDATE / DELETE with parameterised binding. - Returns True on success, False on failure after retries. - """ - for attempt in range(self.MAX_RETRIES): - try: - with self._connect() as conn: - with conn.cursor() as cur: - cur.execute(sql, params) - conn.commit() - return True - except Exception as exc: - _log.warning("execute attempt %d/%d: %s", attempt + 1, self.MAX_RETRIES, exc) - if attempt < self.MAX_RETRIES - 1: - time.sleep(self.RETRY_DELAY) - _log.error("execute failed after %d attempts", self.MAX_RETRIES) - return False - - def query(self, sql: str, params: tuple = ()) -> list[dict[str, Any]]: - """ - Run SELECT with parameterised binding. Returns list[dict]. - """ - for attempt in range(self.MAX_RETRIES): - try: - with self._connect() as conn: - with conn.cursor(as_dict=True) as cur: - cur.execute(sql, params) - return cur.fetchall() or [] - except Exception as exc: - _log.warning("query attempt %d/%d: %s", attempt + 1, self.MAX_RETRIES, exc) - if attempt < self.MAX_RETRIES - 1: - time.sleep(self.RETRY_DELAY) - _log.error("query failed after %d attempts", self.MAX_RETRIES) - return [] - - def scalar(self, sql: str, params: tuple = ()) -> Any: - """Return first column of first row, or None. Tuple cursor avoids unnamed-column issues.""" - for attempt in range(self.MAX_RETRIES): - try: - with self._connect() as conn: - with conn.cursor() as cur: - cur.execute(sql, params) - row = cur.fetchone() - return row[0] if row else None - except Exception as exc: - _log.warning("scalar attempt %d/%d: %s", attempt + 1, self.MAX_RETRIES, exc) - if attempt < self.MAX_RETRIES - 1: - time.sleep(self.RETRY_DELAY) - return None - - def execute_scalar(self, sql: str, params: tuple = ()) -> Any: - """Alias for scalar() — v1.x compatibility.""" - return self.scalar(sql, params) - - def ping(self) -> bool: - """Test connectivity. Returns True if reachable.""" - try: - return self.scalar("SELECT 1") == 1 - except Exception: - return False - - @property - def backend(self) -> str: - return self._backend - - -# ── Concrete: Microsoft SQL Server via pymssql ──────────────────────────────── - -class MSSQLConnector(SQLConnector): - """ - SQL Server connector using pymssql (native TDS, no sqlcmd dependency). - One connection per call — pymssql is not thread-safe with shared connections. - """ - - def _connect(self) -> Any: - cfg = self._cfg - return pymssql.connect( - server=cfg['server'], - port=cfg['port'], - user=cfg['user'], - password=cfg['password'], - database=cfg['database'], - timeout=30, - login_timeout=10, - tds_version='7.4', - ) - - -# ── Factory ─────────────────────────────────────────────────────────────────── - -def get_connector(backend: str = 'cloud') -> SQLConnector: - """ - Factory: returns the appropriate SQLConnector for the given backend. - Add new database types here without changing callers. - """ - return MSSQLConnector(backend) - - -# ── Self-test ───────────────────────────────────────────────────────────────── - -if __name__ == '__main__': - import sys - print("sql_connector v2.0 — self-test") - for profile in ['cloud', 'local']: - try: - db = get_connector(profile) - ok = db.ping() - print(f" {profile}: {'✅ connected' if ok else '⚠️ ping returned False'}") - except Exception as e: - print(f" {profile}: ❌ {e}") - sys.exit(0)