htm 0.0.20 → 0.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +60 -0
- data/Rakefile +104 -18
- data/db/migrate/00001_enable_extensions.rb +9 -5
- data/db/migrate/00002_create_robots.rb +18 -6
- data/db/migrate/00003_create_file_sources.rb +30 -17
- data/db/migrate/00004_create_nodes.rb +60 -48
- data/db/migrate/00005_create_tags.rb +24 -12
- data/db/migrate/00006_create_node_tags.rb +28 -13
- data/db/migrate/00007_create_robot_nodes.rb +40 -26
- data/db/schema.sql +17 -1
- data/db/seeds.rb +33 -33
- data/docs/database/naming-convention.md +244 -0
- data/docs/database_rake_tasks.md +31 -0
- data/docs/development/rake-tasks.md +80 -35
- data/docs/guides/mcp-server.md +70 -1
- data/examples/.envrc +6 -0
- data/examples/.gitignore +2 -0
- data/examples/00_create_examples_db.rb +94 -0
- data/examples/{basic_usage.rb → 01_basic_usage.rb} +12 -16
- data/examples/{custom_llm_configuration.rb → 03_custom_llm_configuration.rb} +13 -3
- data/examples/{file_loader_usage.rb → 04_file_loader_usage.rb} +11 -14
- data/examples/{timeframe_demo.rb → 05_timeframe_demo.rb} +10 -3
- data/examples/{example_app → 06_example_app}/app.rb +15 -15
- data/examples/{cli_app → 07_cli_app}/htm_cli.rb +15 -22
- data/examples/08_sinatra_app/Gemfile.lock +241 -0
- data/examples/{sinatra_app → 08_sinatra_app}/app.rb +19 -18
- data/examples/{mcp_client.rb → 09_mcp_client.rb} +5 -8
- data/examples/{telemetry → 10_telemetry}/SETUP_README.md +1 -1
- data/examples/{telemetry → 10_telemetry}/demo.rb +14 -10
- data/examples/11_robot_groups/README.md +335 -0
- data/examples/{robot_groups → 11_robot_groups/lib}/robot_worker.rb +17 -3
- data/examples/{robot_groups → 11_robot_groups}/multi_process.rb +9 -9
- data/examples/{robot_groups → 11_robot_groups}/same_process.rb +9 -12
- data/examples/{rails_app → 12_rails_app}/Gemfile +3 -0
- data/examples/{rails_app → 12_rails_app}/Gemfile.lock +87 -58
- data/examples/{rails_app → 12_rails_app}/app/controllers/dashboard_controller.rb +10 -6
- data/examples/{rails_app → 12_rails_app}/app/controllers/files_controller.rb +5 -5
- data/examples/{rails_app → 12_rails_app}/app/controllers/memories_controller.rb +11 -7
- data/examples/{rails_app → 12_rails_app}/app/controllers/robots_controller.rb +8 -8
- data/examples/12_rails_app/app/controllers/tags_controller.rb +36 -0
- data/examples/{rails_app → 12_rails_app}/app/views/dashboard/index.html.erb +2 -2
- data/examples/{rails_app → 12_rails_app}/app/views/files/new.html.erb +5 -2
- data/examples/{rails_app → 12_rails_app}/app/views/memories/_memory_card.html.erb +3 -3
- data/examples/{rails_app → 12_rails_app}/app/views/memories/deleted.html.erb +3 -3
- data/examples/{rails_app → 12_rails_app}/app/views/memories/edit.html.erb +3 -3
- data/examples/{rails_app → 12_rails_app}/app/views/memories/show.html.erb +4 -4
- data/examples/{rails_app → 12_rails_app}/app/views/robots/index.html.erb +2 -2
- data/examples/{rails_app → 12_rails_app}/app/views/robots/show.html.erb +4 -4
- data/examples/{rails_app → 12_rails_app}/app/views/search/index.html.erb +1 -1
- data/examples/{rails_app → 12_rails_app}/app/views/tags/index.html.erb +2 -2
- data/examples/{rails_app → 12_rails_app}/app/views/tags/show.html.erb +1 -1
- data/examples/12_rails_app/config/initializers/htm.rb +7 -0
- data/examples/12_rails_app/config/initializers/rack.rb +5 -0
- data/examples/README.md +230 -211
- data/examples/examples_helper.rb +138 -0
- data/lib/htm/config/builder.rb +167 -0
- data/lib/htm/config/database.rb +317 -0
- data/lib/htm/config/defaults.yml +37 -9
- data/lib/htm/config/section.rb +74 -0
- data/lib/htm/config/validator.rb +83 -0
- data/lib/htm/config.rb +64 -360
- data/lib/htm/database.rb +85 -127
- data/lib/htm/errors.rb +14 -0
- data/lib/htm/integrations/sinatra.rb +13 -44
- data/lib/htm/jobs/generate_embedding_job.rb +3 -4
- data/lib/htm/jobs/generate_propositions_job.rb +4 -5
- data/lib/htm/jobs/generate_tags_job.rb +16 -15
- data/lib/htm/loaders/defaults_loader.rb +23 -0
- data/lib/htm/loaders/markdown_loader.rb +17 -15
- data/lib/htm/loaders/xdg_config_loader.rb +9 -9
- data/lib/htm/long_term_memory/fulltext_search.rb +14 -14
- data/lib/htm/long_term_memory/hybrid_search.rb +396 -229
- data/lib/htm/long_term_memory/node_operations.rb +24 -23
- data/lib/htm/long_term_memory/relevance_scorer.rb +23 -20
- data/lib/htm/long_term_memory/robot_operations.rb +4 -4
- data/lib/htm/long_term_memory/tag_operations.rb +91 -77
- data/lib/htm/long_term_memory/vector_search.rb +4 -5
- data/lib/htm/long_term_memory.rb +13 -13
- data/lib/htm/mcp/cli.rb +115 -8
- data/lib/htm/mcp/resources.rb +4 -3
- data/lib/htm/mcp/server.rb +5 -4
- data/lib/htm/mcp/tools.rb +37 -28
- data/lib/htm/migration.rb +72 -0
- data/lib/htm/models/file_source.rb +52 -31
- data/lib/htm/models/node.rb +224 -108
- data/lib/htm/models/node_tag.rb +49 -28
- data/lib/htm/models/robot.rb +38 -27
- data/lib/htm/models/robot_node.rb +63 -35
- data/lib/htm/models/tag.rb +126 -123
- data/lib/htm/observability.rb +45 -41
- data/lib/htm/proposition_service.rb +76 -7
- data/lib/htm/railtie.rb +2 -2
- data/lib/htm/robot_group.rb +30 -18
- data/lib/htm/sequel_config.rb +215 -0
- data/lib/htm/sql_builder.rb +14 -16
- data/lib/htm/tag_service.rb +78 -0
- data/lib/htm/tasks.rb +3 -0
- data/lib/htm/version.rb +1 -1
- data/lib/htm/workflows/remember_workflow.rb +6 -5
- data/lib/htm.rb +26 -22
- data/lib/tasks/db.rake +0 -2
- data/lib/tasks/doc.rake +2 -2
- data/lib/tasks/files.rake +11 -18
- data/lib/tasks/htm.rake +190 -62
- data/lib/tasks/jobs.rake +179 -54
- data/lib/tasks/tags.rake +8 -13
- data/scripts/backfill_parent_tags.rb +376 -0
- data/scripts/normalize_plural_tags.rb +335 -0
- metadata +109 -80
- data/examples/rails_app/app/controllers/tags_controller.rb +0 -30
- data/examples/sinatra_app/Gemfile.lock +0 -166
- data/lib/htm/active_record_config.rb +0 -104
- /data/examples/{config_file_example → 02_config_file_example}/README.md +0 -0
- /data/examples/{config_file_example → 02_config_file_example}/config/htm.local.yml +0 -0
- /data/examples/{config_file_example → 02_config_file_example}/custom_config.yml +0 -0
- /data/examples/{config_file_example → 02_config_file_example}/show_config.rb +0 -0
- /data/examples/{example_app → 06_example_app}/Rakefile +0 -0
- /data/examples/{cli_app → 07_cli_app}/README.md +0 -0
- /data/examples/{sinatra_app → 08_sinatra_app}/Gemfile +0 -0
- /data/examples/{telemetry → 10_telemetry}/README.md +0 -0
- /data/examples/{telemetry → 10_telemetry}/grafana/dashboards/htm-metrics.json +0 -0
- /data/examples/{rails_app → 12_rails_app}/.gitignore +0 -0
- /data/examples/{rails_app → 12_rails_app}/Procfile.dev +0 -0
- /data/examples/{rails_app → 12_rails_app}/README.md +0 -0
- /data/examples/{rails_app → 12_rails_app}/Rakefile +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/assets/stylesheets/application.css +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/assets/stylesheets/inter-font.css +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/controllers/application_controller.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/controllers/search_controller.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/javascript/application.js +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/javascript/controllers/application.js +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/javascript/controllers/index.js +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/files/index.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/files/show.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/layouts/application.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/memories/index.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/memories/new.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/robots/new.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/shared/_navbar.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/shared/_stat_card.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/bin/dev +0 -0
- /data/examples/{rails_app → 12_rails_app}/bin/rails +0 -0
- /data/examples/{rails_app → 12_rails_app}/bin/rake +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/application.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/boot.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/database.yml +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/environment.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/importmap.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/routes.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/tailwind.config.js +0 -0
- /data/examples/{rails_app → 12_rails_app}/config.ru +0 -0
- /data/examples/{rails_app → 12_rails_app}/log/.keep +0 -0
- /data/examples/{rails_app → 12_rails_app}/tmp/local_secret.txt +0 -0
data/lib/htm/database.rb
CHANGED
|
@@ -6,7 +6,7 @@ require 'set'
|
|
|
6
6
|
|
|
7
7
|
class HTM
|
|
8
8
|
# Database setup and configuration for HTM
|
|
9
|
-
# Handles schema creation and database initialization
|
|
9
|
+
# Handles schema creation and database initialization using Sequel
|
|
10
10
|
class Database
|
|
11
11
|
class << self
|
|
12
12
|
# Set up the HTM database schema
|
|
@@ -17,19 +17,22 @@ class HTM
|
|
|
17
17
|
# @return [void]
|
|
18
18
|
#
|
|
19
19
|
def setup(db_url = nil, run_migrations: true, dump_schema: false)
|
|
20
|
-
require '
|
|
21
|
-
require_relative '
|
|
20
|
+
require 'sequel'
|
|
21
|
+
require_relative 'sequel_config'
|
|
22
22
|
|
|
23
|
-
# Establish
|
|
24
|
-
HTM::
|
|
23
|
+
# Establish Sequel connection (don't load models yet - tables may not exist)
|
|
24
|
+
HTM::SequelConfig.establish_connection!(load_models: false)
|
|
25
25
|
|
|
26
|
-
# Run migrations using
|
|
26
|
+
# Run migrations using Sequel
|
|
27
27
|
if run_migrations
|
|
28
|
-
puts "Running
|
|
29
|
-
|
|
28
|
+
puts "Running Sequel migrations..."
|
|
29
|
+
run_sequel_migrations
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
# Now that tables exist, load models
|
|
33
|
+
HTM::SequelConfig.ensure_models_loaded!
|
|
34
|
+
|
|
35
|
+
puts "HTM database schema created successfully"
|
|
33
36
|
|
|
34
37
|
# Optionally dump schema
|
|
35
38
|
if dump_schema
|
|
@@ -44,15 +47,18 @@ class HTM
|
|
|
44
47
|
# @return [void]
|
|
45
48
|
#
|
|
46
49
|
def migrate(db_url = nil)
|
|
47
|
-
require '
|
|
48
|
-
require_relative '
|
|
50
|
+
require 'sequel'
|
|
51
|
+
require_relative 'sequel_config'
|
|
52
|
+
|
|
53
|
+
# Establish Sequel connection (don't load models - tables may not exist)
|
|
54
|
+
HTM::SequelConfig.establish_connection!(load_models: false)
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
HTM::ActiveRecordConfig.establish_connection!
|
|
56
|
+
run_sequel_migrations
|
|
52
57
|
|
|
53
|
-
|
|
58
|
+
# Load models now that tables exist
|
|
59
|
+
HTM::SequelConfig.ensure_models_loaded!
|
|
54
60
|
|
|
55
|
-
puts "
|
|
61
|
+
puts "Database migrations completed"
|
|
56
62
|
end
|
|
57
63
|
|
|
58
64
|
# Show migration status
|
|
@@ -61,26 +67,26 @@ class HTM
|
|
|
61
67
|
# @return [void]
|
|
62
68
|
#
|
|
63
69
|
def migration_status(db_url = nil)
|
|
64
|
-
require '
|
|
65
|
-
require_relative '
|
|
70
|
+
require 'sequel'
|
|
71
|
+
require_relative 'sequel_config'
|
|
66
72
|
|
|
67
|
-
# Establish
|
|
68
|
-
HTM::
|
|
73
|
+
# Establish Sequel connection (don't load models - we just need the DB)
|
|
74
|
+
HTM::SequelConfig.establish_connection!(load_models: false)
|
|
69
75
|
|
|
70
76
|
migrations_path = File.expand_path('../../db/migrate', __dir__)
|
|
71
77
|
|
|
72
78
|
# Get available migrations from files
|
|
73
79
|
available_migrations = Dir.glob(File.join(migrations_path, '*.rb')).map do |file|
|
|
74
80
|
{
|
|
75
|
-
version: File.basename(file).split('_').first,
|
|
81
|
+
version: File.basename(file).split('_').first.to_i,
|
|
76
82
|
name: File.basename(file, '.rb')
|
|
77
83
|
}
|
|
78
84
|
end.sort_by { |m| m[:version] }
|
|
79
85
|
|
|
80
86
|
# Get applied migrations from database
|
|
81
87
|
applied_versions = begin
|
|
82
|
-
|
|
83
|
-
rescue
|
|
88
|
+
HTM.db[:schema_migrations].select_map(:version).map(&:to_i)
|
|
89
|
+
rescue Sequel::DatabaseError
|
|
84
90
|
[]
|
|
85
91
|
end
|
|
86
92
|
|
|
@@ -92,7 +98,7 @@ class HTM
|
|
|
92
98
|
else
|
|
93
99
|
available_migrations.each do |migration|
|
|
94
100
|
status = applied_versions.include?(migration[:version])
|
|
95
|
-
status_mark = status ? "
|
|
101
|
+
status_mark = status ? "[x]" : "[ ]"
|
|
96
102
|
|
|
97
103
|
puts "#{status_mark} #{migration[:name]}"
|
|
98
104
|
end
|
|
@@ -125,38 +131,36 @@ class HTM
|
|
|
125
131
|
tables.each do |table|
|
|
126
132
|
begin
|
|
127
133
|
conn.exec("DROP TABLE IF EXISTS #{table} CASCADE")
|
|
128
|
-
puts "
|
|
134
|
+
puts " Dropped #{table}"
|
|
129
135
|
rescue PG::Error => e
|
|
130
|
-
puts "
|
|
136
|
+
puts " Error dropping #{table}: #{e.message}"
|
|
131
137
|
end
|
|
132
138
|
end
|
|
133
139
|
|
|
134
140
|
# Drop functions and triggers
|
|
135
141
|
begin
|
|
136
142
|
conn.exec("DROP FUNCTION IF EXISTS extract_ontology_topics() CASCADE")
|
|
137
|
-
puts "
|
|
143
|
+
puts " Dropped ontology functions and triggers"
|
|
138
144
|
rescue PG::Error => e
|
|
139
|
-
puts "
|
|
145
|
+
puts " Error dropping functions: #{e.message}"
|
|
140
146
|
end
|
|
141
147
|
|
|
142
148
|
# Drop views
|
|
143
149
|
begin
|
|
144
150
|
conn.exec("DROP VIEW IF EXISTS ontology_structure CASCADE")
|
|
145
151
|
conn.exec("DROP VIEW IF EXISTS topic_relationships CASCADE")
|
|
146
|
-
puts "
|
|
152
|
+
puts " Dropped ontology views"
|
|
147
153
|
rescue PG::Error => e
|
|
148
|
-
puts "
|
|
154
|
+
puts " Error dropping views: #{e.message}"
|
|
149
155
|
end
|
|
150
156
|
|
|
151
157
|
conn.close
|
|
152
|
-
puts "
|
|
158
|
+
puts "All HTM tables dropped"
|
|
153
159
|
end
|
|
154
160
|
|
|
155
161
|
# Seed database with sample data
|
|
156
162
|
#
|
|
157
163
|
# Loads and executes db/seeds.rb file following Rails conventions.
|
|
158
|
-
# All seeding logic is contained in db/seeds.rb and reads data
|
|
159
|
-
# from markdown files in db/seed_data/ directory.
|
|
160
164
|
#
|
|
161
165
|
# @param db_url [String] Database connection URL (uses ENV['HTM_DATABASE__URL'] if not provided)
|
|
162
166
|
# @return [void]
|
|
@@ -165,7 +169,7 @@ class HTM
|
|
|
165
169
|
seeds_file = File.expand_path('../../db/seeds.rb', __dir__)
|
|
166
170
|
|
|
167
171
|
unless File.exist?(seeds_file)
|
|
168
|
-
puts "
|
|
172
|
+
puts "Error: Seeds file not found at #{seeds_file}"
|
|
169
173
|
puts " Please create db/seeds.rb with your seeding logic"
|
|
170
174
|
exit 1
|
|
171
175
|
end
|
|
@@ -189,12 +193,6 @@ class HTM
|
|
|
189
193
|
|
|
190
194
|
puts "Dumping schema to #{schema_file}..."
|
|
191
195
|
|
|
192
|
-
# Build pg_dump command
|
|
193
|
-
# --schema-only: only dump schema, not data
|
|
194
|
-
# --no-owner: don't set ownership
|
|
195
|
-
# --no-privileges: don't dump access privileges
|
|
196
|
-
# --no-tablespaces: don't dump tablespace assignments
|
|
197
|
-
# --exclude-schema=_timescaledb_*: exclude TimescaleDB internal schemas
|
|
198
196
|
env = {
|
|
199
197
|
'PGPASSWORD' => config[:password]
|
|
200
198
|
}
|
|
@@ -214,23 +212,19 @@ class HTM
|
|
|
214
212
|
'-d', config[:dbname]
|
|
215
213
|
]
|
|
216
214
|
|
|
217
|
-
# Execute pg_dump and capture output
|
|
218
215
|
require 'open3'
|
|
219
216
|
stdout, stderr, status = Open3.capture3(env, *cmd)
|
|
220
217
|
|
|
221
218
|
unless status.success?
|
|
222
|
-
puts "
|
|
219
|
+
puts "Error dumping schema:"
|
|
223
220
|
puts stderr
|
|
224
221
|
exit 1
|
|
225
222
|
end
|
|
226
223
|
|
|
227
|
-
# Clean up the output
|
|
228
224
|
cleaned_schema = clean_schema_dump(stdout)
|
|
229
|
-
|
|
230
|
-
# Write to file
|
|
231
225
|
File.write(schema_file, cleaned_schema)
|
|
232
226
|
|
|
233
|
-
puts "
|
|
227
|
+
puts "Schema dumped successfully to #{schema_file}"
|
|
234
228
|
puts " Size: #{File.size(schema_file)} bytes"
|
|
235
229
|
end
|
|
236
230
|
|
|
@@ -248,14 +242,13 @@ class HTM
|
|
|
248
242
|
schema_file = File.expand_path('../../db/schema.sql', __dir__)
|
|
249
243
|
|
|
250
244
|
unless File.exist?(schema_file)
|
|
251
|
-
puts "
|
|
245
|
+
puts "Schema file not found: #{schema_file}"
|
|
252
246
|
puts " Run 'rake htm:db:schema:dump' first to create it"
|
|
253
247
|
exit 1
|
|
254
248
|
end
|
|
255
249
|
|
|
256
250
|
puts "Loading schema from #{schema_file}..."
|
|
257
251
|
|
|
258
|
-
# Build psql command
|
|
259
252
|
env = {
|
|
260
253
|
'PGPASSWORD' => config[:password]
|
|
261
254
|
}
|
|
@@ -270,35 +263,26 @@ class HTM
|
|
|
270
263
|
'--quiet'
|
|
271
264
|
]
|
|
272
265
|
|
|
273
|
-
# Execute psql
|
|
274
266
|
require 'open3'
|
|
275
267
|
stdout, stderr, status = Open3.capture3(env, *cmd)
|
|
276
268
|
|
|
277
269
|
unless status.success?
|
|
278
|
-
puts "
|
|
270
|
+
puts "Error loading schema:"
|
|
279
271
|
puts stderr
|
|
280
272
|
exit 1
|
|
281
273
|
end
|
|
282
274
|
|
|
283
|
-
puts "
|
|
275
|
+
puts "Schema loaded successfully"
|
|
284
276
|
end
|
|
285
277
|
|
|
286
278
|
# Generate database documentation using tbls
|
|
287
279
|
#
|
|
288
|
-
# Uses .tbls.yml configuration file for output directory and settings.
|
|
289
|
-
# Creates comprehensive database documentation including:
|
|
290
|
-
# - Entity-relationship diagrams
|
|
291
|
-
# - Table schemas with comments
|
|
292
|
-
# - Index information
|
|
293
|
-
# - Relationship diagrams
|
|
294
|
-
#
|
|
295
280
|
# @param db_url [String] Database connection URL (uses ENV['HTM_DATABASE__URL'] if not provided)
|
|
296
281
|
# @return [void]
|
|
297
282
|
#
|
|
298
283
|
def generate_docs(db_url = nil)
|
|
299
|
-
# Check if tbls is installed
|
|
300
284
|
unless system('which tbls > /dev/null 2>&1')
|
|
301
|
-
puts "
|
|
285
|
+
puts "Error: 'tbls' is not installed"
|
|
302
286
|
puts ""
|
|
303
287
|
puts "Install tbls:"
|
|
304
288
|
puts " brew install k1LoW/tap/tbls"
|
|
@@ -309,20 +293,17 @@ class HTM
|
|
|
309
293
|
exit 1
|
|
310
294
|
end
|
|
311
295
|
|
|
312
|
-
# Find the project root (where .tbls.yml should be)
|
|
313
296
|
project_root = File.expand_path('../..', __dir__)
|
|
314
297
|
tbls_config = File.join(project_root, '.tbls.yml')
|
|
315
298
|
|
|
316
299
|
unless File.exist?(tbls_config)
|
|
317
|
-
puts "
|
|
300
|
+
puts "Error: .tbls.yml not found at #{tbls_config}"
|
|
318
301
|
exit 1
|
|
319
302
|
end
|
|
320
303
|
|
|
321
|
-
# Get database URL
|
|
322
304
|
dsn = db_url || ENV['HTM_DATABASE__URL']
|
|
323
305
|
raise "Database configuration not found. Set HTM_DATABASE__URL environment variable." unless dsn
|
|
324
306
|
|
|
325
|
-
# Ensure sslmode is set for local development (tbls requires it)
|
|
326
307
|
unless dsn.include?('sslmode=')
|
|
327
308
|
separator = dsn.include?('?') ? '&' : '?'
|
|
328
309
|
dsn = "#{dsn}#{separator}sslmode=disable"
|
|
@@ -330,15 +311,13 @@ class HTM
|
|
|
330
311
|
|
|
331
312
|
puts "Generating database documentation using #{tbls_config}..."
|
|
332
313
|
|
|
333
|
-
# Run tbls doc command with config file and DSN override
|
|
334
|
-
# The --dsn flag overrides the dsn in .tbls.yml but other settings are preserved
|
|
335
314
|
require 'open3'
|
|
336
315
|
cmd = ['tbls', 'doc', '--config', tbls_config, '--dsn', dsn, '--force']
|
|
337
316
|
|
|
338
317
|
stdout, stderr, status = Open3.capture3(*cmd)
|
|
339
318
|
|
|
340
319
|
unless status.success?
|
|
341
|
-
puts "
|
|
320
|
+
puts "Error generating documentation:"
|
|
342
321
|
puts stderr
|
|
343
322
|
puts stdout
|
|
344
323
|
exit 1
|
|
@@ -346,9 +325,8 @@ class HTM
|
|
|
346
325
|
|
|
347
326
|
puts stdout if stdout && !stdout.empty?
|
|
348
327
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
puts "✓ Database documentation generated successfully"
|
|
328
|
+
doc_path = 'docs/database'
|
|
329
|
+
puts "Database documentation generated successfully"
|
|
352
330
|
puts ""
|
|
353
331
|
puts "Documentation files:"
|
|
354
332
|
puts " #{doc_path}/README.md - Main documentation"
|
|
@@ -373,7 +351,6 @@ class HTM
|
|
|
373
351
|
puts "\nHTM Database Information (#{HTM.env})"
|
|
374
352
|
puts "=" * 80
|
|
375
353
|
|
|
376
|
-
# Connection info
|
|
377
354
|
puts "\nConnection:"
|
|
378
355
|
puts " Environment: #{HTM.env}"
|
|
379
356
|
puts " Host: #{config[:host]}"
|
|
@@ -381,19 +358,16 @@ class HTM
|
|
|
381
358
|
puts " Database: #{config[:dbname]}"
|
|
382
359
|
puts " User: #{config[:user]}"
|
|
383
360
|
|
|
384
|
-
# PostgreSQL version
|
|
385
361
|
version = conn.exec("SELECT version()").first['version']
|
|
386
362
|
puts "\nPostgreSQL Version:"
|
|
387
363
|
puts " #{version.split(',').first}"
|
|
388
364
|
|
|
389
|
-
# Extensions
|
|
390
365
|
puts "\nExtensions:"
|
|
391
366
|
extensions = conn.exec("SELECT extname, extversion FROM pg_extension ORDER BY extname").to_a
|
|
392
367
|
extensions.each do |ext|
|
|
393
368
|
puts " #{ext['extname']} (#{ext['extversion']})"
|
|
394
369
|
end
|
|
395
370
|
|
|
396
|
-
# Table info
|
|
397
371
|
puts "\nHTM Tables:"
|
|
398
372
|
tables = ['nodes', 'node_tags', 'tags', 'robots', 'robot_nodes', 'file_sources', 'schema_migrations']
|
|
399
373
|
tables.each do |table|
|
|
@@ -405,7 +379,6 @@ class HTM
|
|
|
405
379
|
end
|
|
406
380
|
end
|
|
407
381
|
|
|
408
|
-
# Database size
|
|
409
382
|
db_size = conn.exec(
|
|
410
383
|
"SELECT pg_size_pretty(pg_database_size($1)) AS size",
|
|
411
384
|
[config[:dbname]]
|
|
@@ -427,7 +400,6 @@ class HTM
|
|
|
427
400
|
|
|
428
401
|
uri = URI.parse(url)
|
|
429
402
|
|
|
430
|
-
# Validate URL format
|
|
431
403
|
unless uri.scheme&.match?(/\Apostgres(?:ql)?\z/i)
|
|
432
404
|
raise ArgumentError, "Invalid database URL scheme: #{uri.scheme}. Expected 'postgresql' or 'postgres'."
|
|
433
405
|
end
|
|
@@ -436,7 +408,7 @@ class HTM
|
|
|
436
408
|
raise ArgumentError, "Database URL must include a host"
|
|
437
409
|
end
|
|
438
410
|
|
|
439
|
-
dbname = uri.path&.slice(1..-1)
|
|
411
|
+
dbname = uri.path&.slice(1..-1)
|
|
440
412
|
if dbname.nil? || dbname.empty?
|
|
441
413
|
raise ArgumentError, "Database URL must include a database name (path segment)"
|
|
442
414
|
end
|
|
@@ -482,16 +454,16 @@ class HTM
|
|
|
482
454
|
htm_config = HTM.config
|
|
483
455
|
|
|
484
456
|
if htm_config.database_configured?
|
|
485
|
-
|
|
457
|
+
db_config = htm_config.database_config
|
|
486
458
|
|
|
487
|
-
# Convert
|
|
459
|
+
# Convert to PG-style keys
|
|
488
460
|
{
|
|
489
|
-
host:
|
|
490
|
-
port:
|
|
491
|
-
dbname:
|
|
492
|
-
user:
|
|
493
|
-
password:
|
|
494
|
-
sslmode:
|
|
461
|
+
host: db_config[:host],
|
|
462
|
+
port: db_config[:port],
|
|
463
|
+
dbname: db_config[:database],
|
|
464
|
+
user: db_config[:username],
|
|
465
|
+
password: db_config[:password],
|
|
466
|
+
sslmode: db_config[:sslmode] || 'prefer'
|
|
495
467
|
}
|
|
496
468
|
elsif ENV['HTM_DATABASE__URL']
|
|
497
469
|
parse_connection_url(ENV['HTM_DATABASE__URL'])
|
|
@@ -506,38 +478,46 @@ class HTM
|
|
|
506
478
|
# Check pgvector
|
|
507
479
|
pgvector = conn.exec("SELECT extversion FROM pg_extension WHERE extname='vector'").first
|
|
508
480
|
if pgvector
|
|
509
|
-
puts "
|
|
481
|
+
puts "pgvector version: #{pgvector['extversion']}"
|
|
510
482
|
else
|
|
511
|
-
puts "
|
|
483
|
+
puts "Warning: pgvector extension not found"
|
|
512
484
|
end
|
|
513
485
|
|
|
514
486
|
# Check pg_trgm
|
|
515
487
|
pg_trgm = conn.exec("SELECT extversion FROM pg_extension WHERE extname='pg_trgm'").first
|
|
516
488
|
if pg_trgm
|
|
517
|
-
puts "
|
|
489
|
+
puts "pg_trgm version: #{pg_trgm['extversion']}"
|
|
490
|
+
else
|
|
491
|
+
puts "Warning: pg_trgm extension not found"
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Check pg_search (BM25 full-text search)
|
|
495
|
+
pg_search = conn.exec("SELECT extversion FROM pg_extension WHERE extname='pg_search'").first
|
|
496
|
+
if pg_search
|
|
497
|
+
puts "pg_search version: #{pg_search['extversion']}"
|
|
518
498
|
else
|
|
519
|
-
puts "
|
|
499
|
+
puts "Warning: pg_search extension not found"
|
|
520
500
|
end
|
|
521
501
|
end
|
|
522
502
|
|
|
523
|
-
# Run
|
|
503
|
+
# Run Sequel migrations from db/migrate/
|
|
524
504
|
#
|
|
525
505
|
# @return [void]
|
|
526
506
|
#
|
|
527
|
-
def
|
|
507
|
+
def run_sequel_migrations
|
|
528
508
|
migrations_path = File.expand_path('../../db/migrate', __dir__)
|
|
529
509
|
|
|
530
510
|
unless Dir.exist?(migrations_path)
|
|
531
|
-
puts "
|
|
511
|
+
puts "No migrations directory found at #{migrations_path}"
|
|
532
512
|
return
|
|
533
513
|
end
|
|
534
514
|
|
|
535
|
-
|
|
515
|
+
db = HTM.db
|
|
536
516
|
|
|
537
517
|
# Create schema_migrations table if it doesn't exist
|
|
538
|
-
unless
|
|
539
|
-
|
|
540
|
-
|
|
518
|
+
unless db.table_exists?(:schema_migrations)
|
|
519
|
+
db.create_table(:schema_migrations) do
|
|
520
|
+
String :version, primary_key: true, null: false
|
|
541
521
|
end
|
|
542
522
|
end
|
|
543
523
|
|
|
@@ -550,17 +530,13 @@ class HTM
|
|
|
550
530
|
version = File.basename(file).split('_').first
|
|
551
531
|
name = File.basename(file, '.rb')
|
|
552
532
|
|
|
553
|
-
# Check if already run
|
|
554
|
-
already_run =
|
|
555
|
-
ActiveRecord::Base.sanitize_sql_array(
|
|
556
|
-
["SELECT COUNT(*) FROM schema_migrations WHERE version = ?", version]
|
|
557
|
-
)
|
|
558
|
-
).to_i > 0
|
|
533
|
+
# Check if already run
|
|
534
|
+
already_run = db[:schema_migrations].where(version: version).count > 0
|
|
559
535
|
|
|
560
536
|
if already_run
|
|
561
|
-
puts "
|
|
537
|
+
puts " [x] #{name} (already migrated)"
|
|
562
538
|
else
|
|
563
|
-
puts "
|
|
539
|
+
puts " --> Running #{name}..."
|
|
564
540
|
require file
|
|
565
541
|
|
|
566
542
|
# Get the migration class
|
|
@@ -568,21 +544,17 @@ class HTM
|
|
|
568
544
|
migration_class = Object.const_get(class_name)
|
|
569
545
|
|
|
570
546
|
# Run the migration
|
|
571
|
-
migration = migration_class.new
|
|
572
|
-
migration.
|
|
547
|
+
migration = migration_class.new(db)
|
|
548
|
+
migration.up
|
|
573
549
|
|
|
574
|
-
# Record in schema_migrations
|
|
575
|
-
|
|
576
|
-
ActiveRecord::Base.sanitize_sql_array(
|
|
577
|
-
["INSERT INTO schema_migrations (version) VALUES (?)", version]
|
|
578
|
-
)
|
|
579
|
-
)
|
|
550
|
+
# Record in schema_migrations
|
|
551
|
+
db[:schema_migrations].insert(version: version)
|
|
580
552
|
|
|
581
|
-
puts "
|
|
553
|
+
puts " Completed"
|
|
582
554
|
end
|
|
583
555
|
end
|
|
584
556
|
|
|
585
|
-
puts "
|
|
557
|
+
puts "All migrations completed"
|
|
586
558
|
end
|
|
587
559
|
|
|
588
560
|
# Clean up pg_dump output to make it more readable
|
|
@@ -594,18 +566,15 @@ class HTM
|
|
|
594
566
|
lines = schema_dump.split("\n")
|
|
595
567
|
cleaned = []
|
|
596
568
|
|
|
597
|
-
# Add header
|
|
598
569
|
cleaned << "-- HTM Database Schema"
|
|
599
570
|
cleaned << "-- Auto-generated from database using pg_dump"
|
|
600
571
|
cleaned << "-- DO NOT EDIT THIS FILE MANUALLY"
|
|
601
572
|
cleaned << "-- Run 'rake htm:db:schema:dump' to regenerate"
|
|
602
573
|
cleaned << ""
|
|
603
574
|
|
|
604
|
-
# Skip pg_dump header comments
|
|
605
575
|
skip_until_content = true
|
|
606
576
|
|
|
607
577
|
lines.each do |line|
|
|
608
|
-
# Skip header comments
|
|
609
578
|
if skip_until_content
|
|
610
579
|
if line =~ /^(SET|CREATE|ALTER|--\s*Name:|COMMENT)/
|
|
611
580
|
skip_until_content = false
|
|
@@ -614,29 +583,18 @@ class HTM
|
|
|
614
583
|
end
|
|
615
584
|
end
|
|
616
585
|
|
|
617
|
-
# Skip SET commands (session-specific settings)
|
|
618
586
|
next if line =~ /^SET /
|
|
619
|
-
|
|
620
|
-
# Skip SELECT pg_catalog.set_config
|
|
621
587
|
next if line =~ /^SELECT pg_catalog\.set_config/
|
|
622
|
-
|
|
623
|
-
# Skip extension comments (we keep extension creation)
|
|
624
588
|
next if line =~ /^COMMENT ON EXTENSION/
|
|
625
589
|
|
|
626
|
-
# Keep everything else
|
|
627
590
|
cleaned << line
|
|
628
591
|
end
|
|
629
592
|
|
|
630
|
-
# Remove multiple blank lines
|
|
631
593
|
result = cleaned.join("\n")
|
|
632
594
|
result.gsub!(/\n{3,}/, "\n\n")
|
|
633
595
|
|
|
634
596
|
result
|
|
635
597
|
end
|
|
636
|
-
|
|
637
|
-
# Old methods removed - now using ActiveRecord migrations
|
|
638
|
-
# def run_schema(conn) - REMOVED
|
|
639
|
-
# def run_migrations_if_needed(conn) - REMOVED (see run_activerecord_migrations above)
|
|
640
598
|
end
|
|
641
599
|
end
|
|
642
600
|
end
|
data/lib/htm/errors.rb
CHANGED
|
@@ -44,6 +44,20 @@ class HTM
|
|
|
44
44
|
#
|
|
45
45
|
class ValidationError < Error; end
|
|
46
46
|
|
|
47
|
+
# Raised when configuration is invalid or incomplete
|
|
48
|
+
#
|
|
49
|
+
# Common causes:
|
|
50
|
+
# - Invalid HTM_ENV value (not defined in config)
|
|
51
|
+
# - Missing database configuration for environment
|
|
52
|
+
# - HTM_CONF pointing to non-existent file
|
|
53
|
+
# - Invalid YAML syntax in config file
|
|
54
|
+
#
|
|
55
|
+
# @example
|
|
56
|
+
# HTM_ENV=staginr rake htm:db:create # => raises ConfigurationError
|
|
57
|
+
# HTM_ENV=defaults rake htm:db:setup # => raises ConfigurationError
|
|
58
|
+
#
|
|
59
|
+
class ConfigurationError < Error; end
|
|
60
|
+
|
|
47
61
|
# Raised when system resources are exhausted
|
|
48
62
|
#
|
|
49
63
|
# Common causes:
|
|
@@ -100,6 +100,7 @@ class HTM
|
|
|
100
100
|
# Rack middleware for HTM connection management
|
|
101
101
|
#
|
|
102
102
|
# Ensures database connections are properly managed across requests.
|
|
103
|
+
# With Sequel's fiber-safe connection pooling, this is largely automatic.
|
|
103
104
|
#
|
|
104
105
|
# @example Use in Sinatra app
|
|
105
106
|
# class MyApp < Sinatra::Base
|
|
@@ -107,70 +108,38 @@ class HTM
|
|
|
107
108
|
# end
|
|
108
109
|
#
|
|
109
110
|
class Middleware
|
|
110
|
-
# Class-level storage for connection configuration (shared across threads)
|
|
111
|
-
@@db_config = nil
|
|
112
|
-
@@config_mutex = Mutex.new
|
|
113
|
-
|
|
114
111
|
def initialize(app, options = {})
|
|
115
112
|
@app = app
|
|
116
113
|
@options = options
|
|
117
114
|
end
|
|
118
115
|
|
|
119
116
|
def call(env)
|
|
120
|
-
# Ensure connection is available
|
|
121
|
-
|
|
117
|
+
# Ensure connection is available
|
|
118
|
+
ensure_connection!
|
|
122
119
|
|
|
123
120
|
# Process request
|
|
124
121
|
status, headers, body = @app.call(env)
|
|
125
122
|
|
|
126
123
|
# Return response
|
|
127
124
|
[status, headers, body]
|
|
128
|
-
ensure
|
|
129
|
-
# Return connections to pool after request completes
|
|
130
|
-
if defined?(ActiveRecord::Base) && ActiveRecord::Base.respond_to?(:connection_handler)
|
|
131
|
-
ActiveRecord::Base.connection_handler.clear_active_connections!
|
|
132
|
-
end
|
|
133
125
|
end
|
|
134
126
|
|
|
135
127
|
# Store the connection config at startup (called from register_htm)
|
|
136
128
|
def self.store_config!
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
@@db_config = HTM::ActiveRecordConfig.load_database_config
|
|
141
|
-
end
|
|
129
|
+
# With Sequel, connection is established globally via HTM::SequelConfig
|
|
130
|
+
# No additional per-request config storage needed
|
|
142
131
|
end
|
|
143
132
|
|
|
144
133
|
private
|
|
145
134
|
|
|
146
|
-
def
|
|
147
|
-
#
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
rescue ActiveRecord::ConnectionNotDefined
|
|
152
|
-
false
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
if pool_exists
|
|
156
|
-
return if ActiveRecord::Base.connection_pool.active_connection?
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
# Re-establish connection using stored config
|
|
160
|
-
if @@db_config
|
|
161
|
-
ActiveRecord::Base.establish_connection(@@db_config)
|
|
162
|
-
else
|
|
163
|
-
raise "HTM database config not stored - call register_htm at app startup"
|
|
164
|
-
end
|
|
165
|
-
rescue ActiveRecord::ConnectionNotDefined, ActiveRecord::ConnectionNotEstablished
|
|
166
|
-
# Pool doesn't exist, establish connection
|
|
167
|
-
if @@db_config
|
|
168
|
-
ActiveRecord::Base.establish_connection(@@db_config)
|
|
169
|
-
else
|
|
170
|
-
raise "HTM database config not stored - call register_htm at app startup"
|
|
135
|
+
def ensure_connection!
|
|
136
|
+
# Sequel handles connection pooling automatically
|
|
137
|
+
# Just verify the connection is available
|
|
138
|
+
unless HTM.db
|
|
139
|
+
HTM::SequelConfig.establish_connection!
|
|
171
140
|
end
|
|
172
141
|
rescue StandardError => e
|
|
173
|
-
HTM.logger.error "Failed to ensure
|
|
142
|
+
HTM.logger.error "Failed to ensure connection: #{e.class} - #{e.message}"
|
|
174
143
|
raise
|
|
175
144
|
end
|
|
176
145
|
end
|
|
@@ -212,10 +181,10 @@ module ::Sinatra
|
|
|
212
181
|
end
|
|
213
182
|
end
|
|
214
183
|
|
|
215
|
-
#
|
|
184
|
+
# Establish initial connection (Sequel handles pooling automatically)
|
|
216
185
|
begin
|
|
217
186
|
HTM::Sinatra::Middleware.store_config!
|
|
218
|
-
HTM::
|
|
187
|
+
HTM::SequelConfig.establish_connection!
|
|
219
188
|
HTM.logger.info "HTM database connection established"
|
|
220
189
|
rescue StandardError => e
|
|
221
190
|
HTM.logger.error "Failed to establish HTM database connection: #{e.message}"
|