sqlite-db
SQLiteデータベースを構築、スキーマ設計、データ抽出、関係性管理する際に、sqlite3 CLIの基本的な使い方をガイドし、専門的なSQLiteスキルを構築するための土台となるパターンを提供するSkill。
📜 元の英語説明(参考)
General guide for using the sqlite3 CLI to build composable knowledge databases. Use this skill when creating SQLite databases, designing schemas, querying data, managing relationships, or building new sqlite-based domain skills. Provides the foundational patterns that all specialized sqlite skills build upon.
🇯🇵 日本人クリエイター向け解説
SQLiteデータベースを構築、スキーマ設計、データ抽出、関係性管理する際に、sqlite3 CLIの基本的な使い方をガイドし、専門的なSQLiteスキルを構築するための土台となるパターンを提供するSkill。
※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。
下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o sqlite-db.zip https://jpskill.com/download/23574.zip && unzip -o sqlite-db.zip && rm sqlite-db.zip
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/23574.zip -OutFile "$d\sqlite-db.zip"; Expand-Archive "$d\sqlite-db.zip" -DestinationPath $d -Force; ri "$d\sqlite-db.zip"
完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。
💾 手動でダウンロードしたい(コマンドが難しい人向け)
- 1. 下の青いボタンを押して
sqlite-db.zipをダウンロード - 2. ZIPファイルをダブルクリックで解凍 →
sqlite-dbフォルダができる - 3. そのフォルダを
C:\Users\あなたの名前\.claude\skills\(Win)または~/.claude/skills/(Mac)へ移動 - 4. Claude Code を再起動
⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。
🎯 このSkillでできること
下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。
📦 インストール方法 (3ステップ)
- 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
- 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
- 3. 展開してできたフォルダを、ホームフォルダの
.claude/skills/に置く- · macOS / Linux:
~/.claude/skills/ - · Windows:
%USERPROFILE%\.claude\skills\
- · macOS / Linux:
Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。
詳しい使い方ガイドを見る →- 最終更新
- 2026-05-18
- 取得日時
- 2026-05-18
- 同梱ファイル
- 1
📖 Skill本文(日本語訳)
※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。
[Skill 名] sqlite-db
SQLite データベーススキル
生の SQL を介した構成可能な知識データベース。
SQLite データベースはポータブルで自己完結型であり、サーバーを必要としません。sqlite3 CLI は、リレーショナル SQL の全機能(インデックス、結合、集計、ウィンドウ関数、CTE、全文検索、JSON 関数、トリガー、ビュー)への直接アクセスを提供します。このスキルは、エージェントが SQLite を知識管理基盤として使用する方法を教えます。
哲学
SQL がインターフェースです
ラッパーも抽象化レイヤーもありません。SQL を直接構成します。これにより、SQLite の全機能(複雑な結合、ウィンドウ関数、CTE、FTS5、JSON 操作、トリガー、ビュー)を最大限に活用できます。冗長性はキーストロークではなくトークンを消費しますが、その表現力は大きな利益をもたらします。
スキーマは DDL です
YAML 宣言はありません。CREATE TABLE ステートメントがスキーマです。.schema を実行するとすべてが表示されます。列の型、制約、外部キー、インデックス — すべて DDL で確認できます。設計上、自己文書化されています。
構成可能な .db ファイル
各ドメインは独自の .db ファイルを持ちます。メモデータベース、投資トラッカー、課題トラッカーは別々のファイルです。ポータブルで、個別にコピー、共有、バックアップできます。中央サーバーは必要ありません。
エージェント互換
sqlite3 CLI は、呼び出しごとに決定論的でステートレスです。出力モード(-header -column、.mode json、-line)は解析可能です。コマンドはセッション状態に依存しません。LLM 駆動のワークフローに最適です。
データベースのターゲット設定
データベースパスは常に sqlite3 の最初の引数として渡してください。これにより、ステートレスで決定論的な動作が保証されます。
# 単一行コマンド
sqlite3 /path/to/mydata.db "SELECT * FROM notes WHERE status = 'active';"
# ヒアドキュメントによる複数行コマンド
sqlite3 /path/to/mydata.db <<'SQL'
SELECT id, title, created_at
FROM notes
WHERE status = 'active'
ORDER BY created_at DESC;
SQL
出力モード
ニーズに基づいて出力モードを選択してください。
| モード | ユースケース | 呼び出し |
|---|---|---|
| Column | 人間が読めるテーブル | sqlite3 -header -column mydata.db "SELECT ..." |
| JSON | jq を使用したエージェント解析 | sqlite3 mydata.db "SELECT ..." \| jq (.mode json の後) |
| CSV | スプレッドシートへのエクスポート | sqlite3 -csv -header mydata.db "SELECT ..." |
| Line | 単一レコードの検査 | sqlite3 -line mydata.db "SELECT * FROM notes WHERE id = 'NOTE-...';" |
JSON モードの例
# JSON 出力を有効にしてクエリを実行
sqlite3 /path/to/mydata.db <<'SQL'
.mode json
SELECT id, title, tags FROM notes LIMIT 5;
SQL
次に、フィルタリングまたは変換のために jq にパイプします。
sqlite3 /path/to/mydata.db "SELECT ..." | jq -r '.[] | select(.status == "active") | .id'
コア操作
データベースの初期化
データベースディレクトリを作成し、制約とプラグマを使用してテーブルを初期化します。
# ディレクトリの作成
mkdir -p /path/to/.sqlite
# プラグマとスキーマでデータベースを初期化
sqlite3 /path/to/.sqlite/mydata.db <<'SQL'
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS notes (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
body TEXT,
status TEXT NOT NULL CHECK (status IN ('draft', 'active', 'archived')) DEFAULT 'draft',
tags TEXT CHECK (json_valid(tags)),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_notes_status ON notes(status);
CREATE INDEX IF NOT EXISTS idx_notes_created ON notes(created_at DESC);
SQL
重要なプラグマ:
PRAGMA journal_mode = WAL;— 同時読み取りとパフォーマンスの向上を可能にしますPRAGMA foreign_keys = ON;— 参照整合性を強制します
ID の生成
インライン SQL 式を使用して、一意で時間順の人間が読める ID を生成します。
'PREFIX-' || strftime('%Y%m%d', 'now') || '-' || lower(hex(randomblob(4)))
例:
'NOTE-' || strftime('%Y%m%d', 'now') || '-' || lower(hex(randomblob(4)))→NOTE-20260208-a3f8c291'TASK-' || strftime('%Y%m%d', 'now') || '-' || lower(hex(randomblob(4)))→TASK-20260208-7b2e9f41
プレフィックスの慣例:
- Notes:
NOTE- - Tasks:
TASK- - Resources:
RES- - Clippings:
CLIP- - Breadcrumbs:
CRUMB- - Reflections:
REFL-
レコードの作成
インライン ID 生成でレコードを挿入します。
sqlite3 /path/to/.sqlite/mydata.db <<'SQL'
INSERT INTO notes (id, title, body, status, tags)
VALUES (
'NOTE-' || strftime('%Y%m%d', 'now') || '-' || lower(hex(randomblob(4))),
'Understanding Composability',
'Systems that compose are systems that scale...',
'active',
json_array('systems', 'design', 'composability')
);
SQL
注: JSON 配列フィールドには文字列連結ではなく json_array() を使用してください。
レコードのクエリ
# フィルタリングと順序付けを伴うシンプルなクエリ
sqlite3 -header -column /path/to/.sqlite/mydata.db <<'SQL'
SELECT id, title, status, created_at
FROM notes
WHERE status = 'active'
ORDER BY created_at DESC
LIMIT 10;
SQL
ページネーションの例:
SELECT id, title
FROM notes
ORDER BY created_at DESC
LIMIT 20 OFFSET 40; -- 3ページ目 (1ページあたり20件)
スクリプト用の JSON 出力:
sqlite3 /path/to/.sqlite/mydata.db <<'SQL'
.mode json
SELECT id, title, tags FROM notes WHERE status = 'active';
SQL
レコードの表示
人間が読める単一レコード表示には -line モードを使用してください。
sqlite3 -line /path/to/.sqlite/mydata.db <<'SQL'
SELECT * FROM notes WHERE id = 'NOTE-20260208-a3f8c291';
SQL
出力:
id = NOTE-20260208-a3f8c291
title = Understanding Composability
body = Systems that compose are systems that scale...
status = active
tags = ["systems","design","composability"]
created_at = 2026-02-08 14:32:01
updated_at = 2026-02-08 14:32:01
レコードの更新
sqlite3 /path/to/.sqlite/mydata.db <<'SQL'
UPDATE notes
SET
status = 'archived',
updated_at = datetime('now')
WHERE id = 'NOTE-20260208-a3f8c291';
SQL
バッチ更新の例:
UPDATE notes
SET status = 'archived', updated_at = datetime('now')
WHERE created_a 📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開
SQLite Database Skills
Composable knowledge databases via raw SQL.
SQLite databases are portable, self-contained, and require no server. The sqlite3 CLI provides direct access to the full power of relational SQL: indexes, joins, aggregations, window functions, CTEs, full-text search, JSON functions, triggers, and views. This skill teaches agents how to use SQLite as a knowledge management substrate.
Philosophy
SQL is the Interface
No wrapper, no abstraction layer. You compose SQL directly. This gives you the full power of SQLite: complex joins, window functions, CTEs, FTS5, JSON operations, triggers, and views. Verbosity costs tokens, not keystrokes — and the expressiveness pays dividends.
Schemas Are the DDL
No YAML declarations. The CREATE TABLE statements are the schema. Run .schema to see everything. Column types, constraints, foreign keys, indexes — all visible in the DDL. Self-documenting by design.
Composable .db Files
Each domain gets its own .db file. Your notes database, investment tracker, and issue tracker are separate files. Portable — copy, share, back up independently. No central server required.
Agent-Compatible
The sqlite3 CLI is deterministic and stateless per invocation. Output modes (-header -column, .mode json, -line) are parseable. Commands never rely on session state. Perfect for LLM-driven workflows.
Database Targeting
Always pass the database path as the first argument to sqlite3. This ensures stateless, deterministic behavior.
# Single-line command
sqlite3 /path/to/mydata.db "SELECT * FROM notes WHERE status = 'active';"
# Multi-line command via heredoc
sqlite3 /path/to/mydata.db <<'SQL'
SELECT id, title, created_at
FROM notes
WHERE status = 'active'
ORDER BY created_at DESC;
SQL
Output Modes
Choose the output mode based on your needs:
| Mode | Use Case | Invocation |
|---|---|---|
| Column | Human-readable tables | sqlite3 -header -column mydata.db "SELECT ..." |
| JSON | Agent parsing with jq | sqlite3 mydata.db "SELECT ..." \| jq (after .mode json) |
| CSV | Export to spreadsheets | sqlite3 -csv -header mydata.db "SELECT ..." |
| Line | Single record inspection | sqlite3 -line mydata.db "SELECT * FROM notes WHERE id = 'NOTE-...';" |
JSON Mode Example
# Enable JSON output and query
sqlite3 /path/to/mydata.db <<'SQL'
.mode json
SELECT id, title, tags FROM notes LIMIT 5;
SQL
Then pipe to jq for filtering or transformation:
sqlite3 /path/to/mydata.db "SELECT ..." | jq -r '.[] | select(.status == "active") | .id'
Core Operations
Initialize a Database
Create the database directory and initialize tables with constraints and pragmas:
# Create directory
mkdir -p /path/to/.sqlite
# Initialize database with pragmas and schema
sqlite3 /path/to/.sqlite/mydata.db <<'SQL'
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS notes (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
body TEXT,
status TEXT NOT NULL CHECK (status IN ('draft', 'active', 'archived')) DEFAULT 'draft',
tags TEXT CHECK (json_valid(tags)),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_notes_status ON notes(status);
CREATE INDEX IF NOT EXISTS idx_notes_created ON notes(created_at DESC);
SQL
Important pragmas:
PRAGMA journal_mode = WAL;— enables concurrent reads and better performancePRAGMA foreign_keys = ON;— enforces referential integrity
Generate IDs
Use inline SQL expressions to generate unique, time-ordered, human-readable IDs:
'PREFIX-' || strftime('%Y%m%d', 'now') || '-' || lower(hex(randomblob(4)))
Examples:
'NOTE-' || strftime('%Y%m%d', 'now') || '-' || lower(hex(randomblob(4)))→NOTE-20260208-a3f8c291'TASK-' || strftime('%Y%m%d', 'now') || '-' || lower(hex(randomblob(4)))→TASK-20260208-7b2e9f41
Prefix conventions:
- Notes:
NOTE- - Tasks:
TASK- - Resources:
RES- - Clippings:
CLIP- - Breadcrumbs:
CRUMB- - Reflections:
REFL-
Create Records
Insert records with inline ID generation:
sqlite3 /path/to/.sqlite/mydata.db <<'SQL'
INSERT INTO notes (id, title, body, status, tags)
VALUES (
'NOTE-' || strftime('%Y%m%d', 'now') || '-' || lower(hex(randomblob(4))),
'Understanding Composability',
'Systems that compose are systems that scale...',
'active',
json_array('systems', 'design', 'composability')
);
SQL
Note: Use json_array() for JSON array fields, not string concatenation.
Query Records
# Simple query with filtering and ordering
sqlite3 -header -column /path/to/.sqlite/mydata.db <<'SQL'
SELECT id, title, status, created_at
FROM notes
WHERE status = 'active'
ORDER BY created_at DESC
LIMIT 10;
SQL
Pagination example:
SELECT id, title
FROM notes
ORDER BY created_at DESC
LIMIT 20 OFFSET 40; -- Page 3 (20 per page)
JSON output for scripting:
sqlite3 /path/to/.sqlite/mydata.db <<'SQL'
.mode json
SELECT id, title, tags FROM notes WHERE status = 'active';
SQL
Show a Record
Use -line mode for human-readable single-record display:
sqlite3 -line /path/to/.sqlite/mydata.db <<'SQL'
SELECT * FROM notes WHERE id = 'NOTE-20260208-a3f8c291';
SQL
Output:
id = NOTE-20260208-a3f8c291
title = Understanding Composability
body = Systems that compose are systems that scale...
status = active
tags = ["systems","design","composability"]
created_at = 2026-02-08 14:32:01
updated_at = 2026-02-08 14:32:01
Update Records
sqlite3 /path/to/.sqlite/mydata.db <<'SQL'
UPDATE notes
SET
status = 'archived',
updated_at = datetime('now')
WHERE id = 'NOTE-20260208-a3f8c291';
SQL
Batch update example:
UPDATE notes
SET status = 'archived', updated_at = datetime('now')
WHERE created_at < date('now', '-1 year');
Delete Records
sqlite3 /path/to/.sqlite/mydata.db <<'SQL'
DELETE FROM notes WHERE id = 'NOTE-20260208-a3f8c291';
SQL
Relationships
SQLite supports two relationship styles, each with distinct use cases.
Structural Relationships (Foreign Key Columns)
Use foreign key columns for parent-child ownership and 1:1 or N:1 relationships:
CREATE TABLE clippings (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
resource_id TEXT, -- Foreign key to resources table
clipped_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (resource_id) REFERENCES resources(id) ON DELETE CASCADE
);
CREATE INDEX idx_clippings_resource ON clippings(resource_id);
Query pattern:
-- All clippings from a specific resource
SELECT c.id, c.content, c.clipped_at
FROM clippings c
WHERE c.resource_id = 'RES-20260208-f1a2b3c4';
-- Join to get resource details
SELECT c.id, c.content, r.title AS resource_title
FROM clippings c
JOIN resources r ON c.resource_id = r.id
WHERE r.status = 'finished';
Flexible Relationships (Links Table)
Use a generic links table for many-to-many, ad-hoc, named relationships:
CREATE TABLE links (
source_id TEXT NOT NULL,
target_id TEXT NOT NULL,
rel_type TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (source_id, target_id, rel_type)
);
CREATE INDEX idx_links_source ON links(source_id, rel_type);
CREATE INDEX idx_links_target ON links(target_id, rel_type);
Create links:
-- Single link
INSERT INTO links (source_id, target_id, rel_type)
VALUES ('NOTE-20260208-a3f8c291', 'NOTE-20260205-b2c3d4e5', 'linksTo');
-- Batch link creation
INSERT INTO links (source_id, target_id, rel_type)
SELECT 'CRUMB-20260208-f1f2f3f4', id, 'analyzedNotes'
FROM notes
WHERE tags LIKE '%systems%' AND created_at > date('now', '-7 days');
Query outgoing links:
SELECT l.rel_type, n.id, n.title
FROM links l
JOIN notes n ON l.target_id = n.id
WHERE l.source_id = 'NOTE-20260208-a3f8c291';
Query incoming links:
SELECT l.rel_type, n.id, n.title
FROM links l
JOIN notes n ON l.source_id = n.id
WHERE l.target_id = 'NOTE-20260208-a3f8c291';
Remove links:
DELETE FROM links
WHERE source_id = 'NOTE-20260208-a3f8c291'
AND target_id = 'NOTE-20260205-b2c3d4e5'
AND rel_type = 'linksTo';
Common relationship types:
linksTo— general connectionderivedFrom— content derived from another notepartOf— belongs to a container/collectionanalyzedNotes— breadcrumb analyzed these notesbasedOnNotes— reflection based on these notespromotedTo— reflection promoted to note
Views as Saved Queries
Views are more powerful than memhub's saved queries — they can use joins, aggregations, and reference other views.
Create a View
CREATE VIEW active_notes AS
SELECT id, title, status, created_at
FROM notes
WHERE status = 'active'
ORDER BY created_at DESC;
Query a View
SELECT * FROM active_notes LIMIT 10;
List All Views
SELECT name FROM sqlite_master WHERE type = 'view';
Drop a View
DROP VIEW IF EXISTS active_notes;
Complex View Example
-- Note graph view with link counts
CREATE VIEW note_graph AS
SELECT
n.id,
n.title,
n.status,
COUNT(DISTINCT lo.target_id) AS outgoing_links,
COUNT(DISTINCT li.source_id) AS incoming_links
FROM notes n
LEFT JOIN links lo ON n.id = lo.source_id
LEFT JOIN links li ON n.id = li.target_id
GROUP BY n.id, n.title, n.status;
Triggers for Automation
Triggers automate repetitive tasks like timestamp updates and FTS synchronization.
Auto-Update Timestamps
CREATE TRIGGER update_notes_timestamp
AFTER UPDATE ON notes
FOR EACH ROW
BEGIN
UPDATE notes SET updated_at = datetime('now') WHERE id = OLD.id;
END;
FTS Sync Triggers
See "Full-Text Search" section below for complete examples.
Full-Text Search (FTS5)
SQLite's FTS5 extension provides ranked full-text search. Memhub cannot do this.
Create FTS Virtual Table
CREATE VIRTUAL TABLE notes_fts USING fts5(
id UNINDEXED,
title,
body,
tags,
content='notes',
content_rowid='rowid'
);
Sync Triggers
Keep the FTS index synchronized with the base table:
-- Trigger: insert
CREATE TRIGGER notes_fts_insert AFTER INSERT ON notes BEGIN
INSERT INTO notes_fts(rowid, id, title, body, tags)
VALUES (NEW.rowid, NEW.id, NEW.title, NEW.body, NEW.tags);
END;
-- Trigger: update
CREATE TRIGGER notes_fts_update AFTER UPDATE ON notes BEGIN
UPDATE notes_fts
SET title = NEW.title, body = NEW.body, tags = NEW.tags
WHERE rowid = OLD.rowid;
END;
-- Trigger: delete
CREATE TRIGGER notes_fts_delete AFTER DELETE ON notes BEGIN
DELETE FROM notes_fts WHERE rowid = OLD.rowid;
END;
Search Queries
-- Simple search
SELECT id, title FROM notes_fts WHERE notes_fts MATCH 'composability';
-- Boolean operators
SELECT id, title FROM notes_fts WHERE notes_fts MATCH 'systems AND composability';
-- Phrase search
SELECT id, title FROM notes_fts WHERE notes_fts MATCH '"knowledge management"';
-- Ranked search with snippets
SELECT
n.id,
n.title,
snippet(notes_fts, 1, '**', '**', '...', 32) AS snippet,
bm25(notes_fts) AS rank
FROM notes_fts
JOIN notes n ON notes_fts.id = n.id
WHERE notes_fts MATCH 'composability'
ORDER BY rank
LIMIT 10;
JSON Functions
SQLite provides robust JSON support for array and object fields.
Creating JSON Arrays
-- Inline array
INSERT INTO notes (id, title, tags)
VALUES (
'NOTE-' || strftime('%Y%m%d', 'now') || '-' || lower(hex(randomblob(4))),
'Example Note',
json_array('tag1', 'tag2', 'tag3')
);
Querying JSON Arrays
-- Check if array contains a value
SELECT id, title
FROM notes
WHERE EXISTS (
SELECT 1 FROM json_each(notes.tags)
WHERE json_each.value = 'systems'
);
-- Count array elements
SELECT id, title, json_array_length(tags) AS tag_count
FROM notes
WHERE json_array_length(tags) > 3;
-- Extract unique tags across all notes
SELECT DISTINCT json_each.value AS tag
FROM notes, json_each(notes.tags)
WHERE notes.status = 'active'
ORDER BY tag;
-- Tag cloud (aggregation)
SELECT
json_each.value AS tag,
COUNT(*) AS note_count
FROM notes, json_each(notes.tags)
GROUP BY json_each.value
ORDER BY note_count DESC;
Updating JSON Arrays
-- Add a tag (append to array)
UPDATE notes
SET tags = json_insert(tags, '$[#]', 'new-tag')
WHERE id = 'NOTE-20260208-a3f8c291';
-- Remove a tag (requires rebuilding array)
UPDATE notes
SET tags = (
SELECT json_group_array(value)
FROM json_each(notes.tags)
WHERE value != 'old-tag'
)
WHERE id = 'NOTE-20260208-a3f8c291';
JSON Validation
Use json_valid() in CHECK constraints:
CREATE TABLE notes (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
tags TEXT CHECK (json_valid(tags)),
metadata TEXT CHECK (json_valid(metadata) OR metadata IS NULL)
);
CHECK Constraints for Validation
CHECK constraints replace YAML enum and pattern validation.
Enum-Style Constraints
CREATE TABLE notes (
id TEXT PRIMARY KEY,
status TEXT NOT NULL CHECK (status IN ('draft', 'active', 'archived')) DEFAULT 'draft',
epistemic TEXT CHECK (epistemic IN ('hypothesis', 'tested', 'validated', 'outdated'))
);
Range Constraints
CREATE TABLE resources (
id TEXT PRIMARY KEY,
rating INTEGER CHECK (rating >= 1 AND rating <= 5)
);
Pattern Constraints (Regex)
SQLite doesn't have native regex in CHECK constraints, but you can validate formats:
CREATE TABLE resources (
id TEXT PRIMARY KEY,
url TEXT CHECK (url LIKE 'http%')
);
For complex validation, use application-level checks before INSERT.
Aggregations
SQLite supports GROUP BY, COUNT, SUM, AVG, MIN, MAX, and more. Memhub cannot do this.
Basic Aggregations
-- Notes per status
SELECT status, COUNT(*) AS count
FROM notes
GROUP BY status;
-- Average rating per resource type
SELECT resource_type, AVG(rating) AS avg_rating
FROM resources
WHERE rating IS NOT NULL
GROUP BY resource_type;
-- Monthly note creation counts
SELECT
strftime('%Y-%m', created_at) AS month,
COUNT(*) AS note_count
FROM notes
GROUP BY month
ORDER BY month DESC;
Advanced Aggregations
-- Tag cloud with percentages
WITH tag_counts AS (
SELECT
json_each.value AS tag,
COUNT(*) AS count
FROM notes, json_each(notes.tags)
GROUP BY json_each.value
)
SELECT
tag,
count,
ROUND(100.0 * count / SUM(count) OVER (), 2) AS percentage
FROM tag_counts
ORDER BY count DESC
LIMIT 20;
Building a SQLite-DB Skill
Specialized sqlite-db skills follow a consistent structure. They teach agents how to manage a specific domain using SQLite.
Directory Layout
skills/sqlite-<domain>/
├── SKILL.md # Skill instructions (when to use, workflows, SQL examples)
├── assets/
│ ├── schema.sql # DDL: tables, indexes, FTS, triggers
│ └── views.sql # Reusable views
├── scripts/
│ ├── setup.sh # Idempotent initialization script
│ └── examples.sh # Demo workflows
└── references/
└── queries.md # Complex query recipes
What a SQLite Skill Should Define
- Tables — DDL for all domain entities with constraints, indexes, and foreign keys
- Indexes — B-tree indexes for common queries, FTS indexes for search
- FTS — Virtual tables and sync triggers for full-text search
- Views — Saved queries as first-class database objects
- Triggers — Auto-timestamps, FTS sync, validation
- Links vocabulary — Named relationship types (same as memhub:
linksTo,derivedFrom,partOf, etc.) - Database path — Where the
.dbfile lives (e.g.,.sqlite/notes.db) - ID prefixes — Conventions for generating human-readable IDs
- Workflows — Step-by-step SQL examples for common tasks
Design Principles
Schemas encode domain knowledge. The DDL is the documentation. Use meaningful column names, CHECK constraints, foreign keys, and indexes. The schema should tell you what's important.
Target database explicitly. Every sqlite3 command should specify the full path to the database. Stateless invocation only.
Views are your menu. Create views for common queries. Views compose — they can reference other views, use joins, and include aggregations. They're more powerful than memhub's saved queries.
Use both relationship styles. Structural foreign keys for ownership (clipping→resource), flexible links table for ad-hoc graph relationships (note→note).
Include setup scripts. Provide an idempotent setup.sh that creates the database, runs the DDL, and initializes views. Users should be able to initialize a working database with one command.
Show real workflows. Don't just list SQL patterns — show the full flow of creating records, linking them, querying, updating, and analyzing over time.
Be honest about tradeoffs. SQL is verbose. String escaping is hazardous. But the power (JOINs, FTS, aggregations, window functions) makes it worthwhile for certain domains.