ruby_llm 2.0.0.rc2 → 2.0.0.rc3
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/README.md +2 -2
- data/lib/generators/ruby_llm/upgrade/legacy_content_sql.rb +34 -0
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +332 -0
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb +171 -0
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb +197 -0
- data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +170 -0
- data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +14 -11
- data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +6 -6
- data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +31 -5
- data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +16 -14
- data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +36 -7
- data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +15 -5
- data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +14 -0
- data/lib/ruby_llm/accounting/usage.rb +9 -0
- data/lib/ruby_llm/agent.rb +10 -9
- data/lib/ruby_llm/aliases.json +26 -4
- data/lib/ruby_llm/attachment.rb +5 -0
- data/lib/ruby_llm/chat.rb +4 -0
- data/lib/ruby_llm/message.rb +14 -5
- data/lib/ruby_llm/models.json +3820 -1346
- data/lib/ruby_llm/protocols/bedrock/async_videos.rb +2 -1
- data/lib/ruby_llm/protocols/chat_completions/rerank.rb +8 -1
- data/lib/ruby_llm/protocols/cohere/rerank.rb +8 -1
- data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +5 -0
- data/lib/ruby_llm/protocols/interactions/tools.rb +3 -1
- data/lib/ruby_llm/providers/deepseek/responses.rb +0 -1
- data/lib/ruby_llm/providers/mistral/ocr.rb +5 -1
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/tasks/ruby_llm.rake +1 -1
- metadata +7 -2
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../legacy_content_sql'
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module Generators
|
|
7
|
+
class OnlineCopyMigration < UpgradeMigration
|
|
8
|
+
class Verification # :nodoc: all
|
|
9
|
+
def initialize(connection:, settings:)
|
|
10
|
+
@connection = connection
|
|
11
|
+
@settings = settings
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def verify
|
|
15
|
+
verify_chats
|
|
16
|
+
verify_content
|
|
17
|
+
verify_usages
|
|
18
|
+
verify_tools
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def verify_chats
|
|
24
|
+
mismatch = %w[provider model_id].map do |column|
|
|
25
|
+
different("copied.#{q(column)}", "original.#{q(column)}", string: true)
|
|
26
|
+
end
|
|
27
|
+
reject_mismatch('chat model', <<~SQL)
|
|
28
|
+
SELECT c.#{pk('chat')} FROM #{table('chat')} c
|
|
29
|
+
LEFT JOIN #{table('model')} original ON original.#{pk('model')} = c.#{q(@settings.fetch('model_foreign_key'))}
|
|
30
|
+
LEFT JOIN #{qt(:ruby_llm_models)} copied ON copied.#{pk('model')} = c.ruby_llm_model_id
|
|
31
|
+
WHERE c.ruby_llm_version = 1
|
|
32
|
+
AND (original.#{pk('model')} IS NULL OR copied.#{pk('model')} IS NULL OR #{mismatch.join(' OR ')})
|
|
33
|
+
SQL
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def verify_content
|
|
37
|
+
raw = source(:content_raw)
|
|
38
|
+
rendered = LegacyContentSQL.new(@connection).render(content: "m.#{q(:content)}", raw:)
|
|
39
|
+
fields = { ruby_llm_content: rendered, raw_content: raw }
|
|
40
|
+
failed = fields.map do |column, expected|
|
|
41
|
+
different("m.#{q(column)}", expected, json: column == :raw_content, string: true)
|
|
42
|
+
end
|
|
43
|
+
reject_mismatch('message content', "SELECT m.#{pk('message')} #{message_from} " \
|
|
44
|
+
"WHERE c.ruby_llm_version = 1 AND (#{failed.join(' OR ')})")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def verify_usages
|
|
48
|
+
fields = usage_fields
|
|
49
|
+
eligible = fields.slice(*%i[input_tokens output_tokens cache_read_tokens cache_write_tokens thinking_tokens
|
|
50
|
+
input_cost output_cost cache_read_cost cache_write_cost thinking_cost total_cost])
|
|
51
|
+
.values.map { |value| "#{value} IS NOT NULL" }
|
|
52
|
+
eligible << "m.#{q(:role)} = 'assistant'"
|
|
53
|
+
strings = %i[chat_type message_type provider model operation status]
|
|
54
|
+
failed = fields.map do |column, expected|
|
|
55
|
+
different("u.#{q(column)}", expected, string: strings.include?(column))
|
|
56
|
+
end
|
|
57
|
+
reject_mismatch('usage entry', <<~SQL)
|
|
58
|
+
SELECT m.#{pk('message')} #{message_from}
|
|
59
|
+
LEFT JOIN #{table('model')} cm ON cm.#{pk('model')} = c.#{q(@settings.fetch('model_foreign_key'))}
|
|
60
|
+
#{message_model_join}
|
|
61
|
+
LEFT JOIN #{qt(:ruby_llm_usages)} u
|
|
62
|
+
ON u.legacy_key = #{copied_key(:ruby_llm_usages, "m.#{pk('message')}")}
|
|
63
|
+
WHERE c.ruby_llm_version = 1 AND (#{eligible.join(' OR ')})
|
|
64
|
+
AND (u.id IS NULL OR #{failed.join(' OR ')})
|
|
65
|
+
SQL
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def usage_fields
|
|
69
|
+
fields = {
|
|
70
|
+
input_tokens: source(:input_tokens), output_tokens: source(:output_tokens),
|
|
71
|
+
cache_read_tokens: coalesce(source(:cache_read_tokens), source(:cached_tokens)),
|
|
72
|
+
cache_write_tokens: coalesce(source(:cache_write_tokens), source(:cache_creation_tokens)),
|
|
73
|
+
thinking_tokens: source(:thinking_tokens)
|
|
74
|
+
}
|
|
75
|
+
%w[input output cache_read cache_write thinking].each { |key| fields[:"#{key}_cost"] = cost(key) }
|
|
76
|
+
fields[:total_cost] = coalesce(source(:total_cost), cost('total'))
|
|
77
|
+
provider, model = model_identity
|
|
78
|
+
fields.merge(chat_type: value(@settings.fetch('chat_class')), chat_id: "c.#{pk('chat')}",
|
|
79
|
+
message_type: value(@settings.fetch('message_class')), message_id: "m.#{pk('message')}",
|
|
80
|
+
provider:, model:, operation: "'chat'", status: "'succeeded'")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def model_identity
|
|
84
|
+
key = @settings.fetch('model_foreign_key')
|
|
85
|
+
if message_columns[key]&.type == :string
|
|
86
|
+
["COALESCE(#{source(:provider)}, cm.provider)", "COALESCE(#{source(key)}, cm.model_id)"]
|
|
87
|
+
elsif message_columns.key?(key)
|
|
88
|
+
['COALESCE(mm.provider, cm.provider)', 'COALESCE(mm.model_id, cm.model_id)']
|
|
89
|
+
else
|
|
90
|
+
['cm.provider', 'cm.model_id']
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def message_model_join
|
|
95
|
+
key = @settings.fetch('model_foreign_key')
|
|
96
|
+
return '' if !message_columns.key?(key) || message_columns[key].type == :string
|
|
97
|
+
|
|
98
|
+
"LEFT JOIN #{table('model')} mm ON mm.#{pk('model')} = m.#{q(key)}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def cost(key)
|
|
102
|
+
return 'NULL' unless message_columns.key?('cost_details')
|
|
103
|
+
|
|
104
|
+
details = source(:cost_details)
|
|
105
|
+
case @connection.adapter_name
|
|
106
|
+
when 'PostgreSQL' then "NULLIF(#{details}::jsonb ->> '#{key}', '')::numeric"
|
|
107
|
+
when 'Mysql2'
|
|
108
|
+
"CAST(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(#{details}, '$.#{key}')), 'null') AS DECIMAL(16, 10))"
|
|
109
|
+
else "json_extract(#{details}, '$.#{key}')"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def verify_tools
|
|
114
|
+
result_type = "CASE WHEN r.#{pk('message')} IS NOT NULL THEN #{value(@settings.fetch('message_class'))} END"
|
|
115
|
+
fields = { message_id: "s.#{q(@settings.fetch('message_foreign_key'))}",
|
|
116
|
+
message_type: value(@settings.fetch('message_class')), result_id: "r.#{pk('message')}",
|
|
117
|
+
result_type:,
|
|
118
|
+
tool_call_id: 's.tool_call_id', name: 's.name', arguments: 's.arguments' }
|
|
119
|
+
strings = %i[message_type result_type tool_call_id name arguments]
|
|
120
|
+
failed = fields.map do |column, expected|
|
|
121
|
+
different("t.#{q(column)}", expected, json: column == :arguments, string: strings.include?(column))
|
|
122
|
+
end
|
|
123
|
+
reject_mismatch('tool call', <<~SQL)
|
|
124
|
+
SELECT s.#{pk('tool_call')} FROM #{table('tool_call')} s
|
|
125
|
+
LEFT JOIN #{table('message')} m ON m.#{pk('message')} = s.#{q(@settings.fetch('message_foreign_key'))}
|
|
126
|
+
LEFT JOIN #{table('chat')} c ON c.#{pk('chat')} = m.#{q(@settings.fetch('chat_foreign_key'))}
|
|
127
|
+
LEFT JOIN #{table('message')} r ON r.#{q(@settings.fetch('tool_call_foreign_key'))} = s.#{pk('tool_call')}
|
|
128
|
+
LEFT JOIN #{qt(:ruby_llm_tool_calls)} t
|
|
129
|
+
ON t.legacy_key = #{copied_key(:ruby_llm_tool_calls, "s.#{pk('tool_call')}")}
|
|
130
|
+
WHERE c.ruby_llm_version = 1 AND (t.id IS NULL OR #{failed.join(' OR ')})
|
|
131
|
+
SQL
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def message_from
|
|
135
|
+
"FROM #{table('message')} m JOIN #{table('chat')} c " \
|
|
136
|
+
"ON c.#{pk('chat')} = m.#{q(@settings.fetch('chat_foreign_key'))}"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def different(left, right, json: false, string: false)
|
|
140
|
+
left, right = comparable([left, right], json:, string:)
|
|
141
|
+
case @connection.adapter_name
|
|
142
|
+
when 'PostgreSQL' then "#{left} IS DISTINCT FROM #{right}"
|
|
143
|
+
when 'Mysql2' then "NOT (#{left} <=> #{right})"
|
|
144
|
+
else "#{left} IS NOT #{right}"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def comparable(expressions, json:, string:)
|
|
149
|
+
case @connection.adapter_name
|
|
150
|
+
when 'PostgreSQL'
|
|
151
|
+
json ? expressions.map { |expression| "(#{expression})::jsonb" } : expressions
|
|
152
|
+
when 'Mysql2'
|
|
153
|
+
string ? expressions.map { |expression| "CAST(#{expression} AS BINARY)" } : expressions
|
|
154
|
+
else
|
|
155
|
+
json ? expressions.map { |expression| "json(#{expression})" } : expressions
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def reject_mismatch(label, sql)
|
|
160
|
+
id = @connection.select_value("#{sql.strip} LIMIT 1")
|
|
161
|
+
raise "Record #{id} did not preserve its #{label}" if id
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def text(expression) = mysql? ? "CAST(#{expression} AS BINARY)" : "CAST(#{expression} AS TEXT)"
|
|
165
|
+
|
|
166
|
+
def copied_key(table, expression)
|
|
167
|
+
return text(expression) unless mysql?
|
|
168
|
+
|
|
169
|
+
collation = @connection.columns(table).find { |column| column.name == 'legacy_key' }.collation
|
|
170
|
+
charset = collation.split('_').first
|
|
171
|
+
"CAST(#{expression} AS CHAR CHARACTER SET #{q(charset)}) COLLATE #{q(collation)}"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def coalesce(*expressions)
|
|
175
|
+
values = expressions.reject { |expression| expression == 'NULL' }
|
|
176
|
+
return 'NULL' if values.empty?
|
|
177
|
+
return values.first if values.one?
|
|
178
|
+
|
|
179
|
+
"COALESCE(#{values.join(', ')})"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def source(column) = message_columns.key?(column.to_s) ? "m.#{q(column)}" : 'NULL'
|
|
183
|
+
|
|
184
|
+
def message_columns
|
|
185
|
+
@message_columns ||= @connection.columns(@settings.fetch('message_table')).index_by(&:name)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def mysql? = @connection.adapter_name == 'Mysql2'
|
|
189
|
+
def table(kind) = qt(@settings.fetch("#{kind}_table"))
|
|
190
|
+
def pk(kind) = q(@connection.primary_key(@settings.fetch("#{kind}_table")))
|
|
191
|
+
def value(value) = @connection.quote(value)
|
|
192
|
+
def q(value) = @connection.quote_column_name(value)
|
|
193
|
+
def qt(value) = @connection.quote_table_name(value)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'upgrade_migration'
|
|
4
|
+
require_relative 'online_copy_migration/journal'
|
|
5
|
+
require_relative 'online_copy_migration/data'
|
|
6
|
+
|
|
7
|
+
module RubyLLM
|
|
8
|
+
module Generators
|
|
9
|
+
class OnlineCopyMigration < UpgradeMigration # :nodoc: all
|
|
10
|
+
def online? = true
|
|
11
|
+
|
|
12
|
+
def prepare(settings)
|
|
13
|
+
with_migration_lock do
|
|
14
|
+
super(settings.merge(online: true), &nil)
|
|
15
|
+
unless @connection.column_exists?(TABLE, :epoch)
|
|
16
|
+
@connection.add_column(TABLE, :epoch, :bigint, null: false, default: 0)
|
|
17
|
+
end
|
|
18
|
+
yield if block_given?
|
|
19
|
+
data.prepare
|
|
20
|
+
journal.install
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def verify_settings(settings)
|
|
25
|
+
super(settings.merge(online: true))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def copy_table(source, target)
|
|
29
|
+
create_copy_table(source, target) unless @connection.table_exists?(target)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def copy_model_references
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def backfill
|
|
37
|
+
with_migration_lock do
|
|
38
|
+
state = states.first!
|
|
39
|
+
unless state.active_version == 1 && %w[preparing active].include?(state.status)
|
|
40
|
+
raise 'Online copy backfill requires RubyLLM 1.16 to be active'
|
|
41
|
+
end
|
|
42
|
+
raise 'Use resume after rolling back a finished upgrade' if data.finished?
|
|
43
|
+
|
|
44
|
+
data.backfill
|
|
45
|
+
data.catch_up(passes: 2)
|
|
46
|
+
state.update!(needs_reconcile: false)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def finish
|
|
51
|
+
with_migration_lock do
|
|
52
|
+
state = states.first!
|
|
53
|
+
return if state.active_version == 2 && state.status == 'active' && data.finished?
|
|
54
|
+
|
|
55
|
+
raise 'Run the copy backfill before finish' unless data.completed?
|
|
56
|
+
|
|
57
|
+
pause(state, from: 1, statuses: %w[preparing active finishing])
|
|
58
|
+
data.catch_up
|
|
59
|
+
data.verify
|
|
60
|
+
verify_completed_work
|
|
61
|
+
yield
|
|
62
|
+
journal.install
|
|
63
|
+
state.update!(active_version: 2, status: 'active', needs_reconcile: false)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def rollback
|
|
68
|
+
with_migration_lock do
|
|
69
|
+
state = states.first!
|
|
70
|
+
unless data.finished?
|
|
71
|
+
raise 'The unfinished upgrade is not running on 1.16' unless state.active_version == 1
|
|
72
|
+
|
|
73
|
+
state.update!(status: 'preparing', needs_reconcile: true)
|
|
74
|
+
return
|
|
75
|
+
end
|
|
76
|
+
state.with_lock do
|
|
77
|
+
require_active_version(state, 2)
|
|
78
|
+
verify_completed_work
|
|
79
|
+
state.update!(active_version: 1, epoch: state.epoch + 1)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def resume
|
|
85
|
+
with_migration_lock do
|
|
86
|
+
raise 'Finish the initial copy migrations before resuming 2.0' unless data.finished?
|
|
87
|
+
|
|
88
|
+
state = states.first!
|
|
89
|
+
pause(state, from: 1, statuses: %w[active finishing])
|
|
90
|
+
data.catch_up
|
|
91
|
+
data.verify
|
|
92
|
+
verify_completed_work
|
|
93
|
+
state.update!(active_version: 2, status: 'active', needs_reconcile: false)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def finalize
|
|
98
|
+
with_migration_lock { super }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def cleanup
|
|
102
|
+
with_migration_lock do
|
|
103
|
+
verify_cleanup
|
|
104
|
+
journal.remove
|
|
105
|
+
yield if block_given?
|
|
106
|
+
data.cleanup
|
|
107
|
+
super
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def pause(state, from:, statuses:)
|
|
114
|
+
state.with_lock do
|
|
115
|
+
unless state.active_version == from && statuses.include?(state.status)
|
|
116
|
+
raise 'The copy upgrade is not ready for this version switch'
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
state.update!(status: 'finishing', epoch: state.epoch + 1) unless state.status == 'finishing'
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def data
|
|
124
|
+
@data ||= Data.new(connection: @connection, settings: configuration)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def journal
|
|
128
|
+
@journal ||= Journal.new(connection: @connection, settings: configuration)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def with_migration_lock(&)
|
|
132
|
+
return with_mysql_lock(&) if @connection.adapter_name == 'Mysql2'
|
|
133
|
+
return with_sqlite_lock(&) if @connection.adapter_name == 'SQLite'
|
|
134
|
+
|
|
135
|
+
key = "hashtextextended(current_database() || '.' || current_schema() || '.ruby_llm_upgrade', 0)"
|
|
136
|
+
locked = @connection.select_value("SELECT pg_try_advisory_lock(#{key})")
|
|
137
|
+
raise 'Another RubyLLM copy migration is running' unless locked
|
|
138
|
+
|
|
139
|
+
begin
|
|
140
|
+
yield
|
|
141
|
+
ensure
|
|
142
|
+
@connection.execute("SELECT pg_advisory_unlock(#{key})")
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def with_mysql_lock
|
|
147
|
+
key = @connection.quote("ruby_llm_upgrade:#{@connection.current_database}".slice(0, 64))
|
|
148
|
+
locked = @connection.select_value("SELECT GET_LOCK(#{key}, 0)")
|
|
149
|
+
raise 'Another RubyLLM copy migration is running' unless locked == 1
|
|
150
|
+
|
|
151
|
+
begin
|
|
152
|
+
yield
|
|
153
|
+
ensure
|
|
154
|
+
@connection.execute("SELECT RELEASE_LOCK(#{key})")
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def with_sqlite_lock
|
|
159
|
+
database = @connection.pool.db_config.database
|
|
160
|
+
return yield if database == ':memory:'
|
|
161
|
+
|
|
162
|
+
File.open("#{database}.ruby_llm_upgrade.lock", File::RDWR | File::CREAT, 0o600) do |file|
|
|
163
|
+
raise 'Another RubyLLM copy migration is running' unless file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
164
|
+
|
|
165
|
+
yield
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'generators/ruby_llm/upgrade/legacy_content_sql'
|
|
2
4
|
<% if copy_mode? -%>
|
|
3
5
|
|
|
4
6
|
require 'generators/ruby_llm/upgrade/upgrade_migration'
|
|
@@ -12,22 +14,19 @@ class <%= backfill_migration_class_name %> < ActiveRecord::Migration<%= migratio
|
|
|
12
14
|
|
|
13
15
|
def up
|
|
14
16
|
<% if copy_mode? -%>
|
|
15
|
-
RubyLLM::Generators::UpgradeMigration.
|
|
17
|
+
RubyLLM::Generators::UpgradeMigration.for.verify_settings(<%= copy_upgrade_settings.inspect %>)
|
|
18
|
+
verify_upgrade_in_progress
|
|
19
|
+
RubyLLM::Generators::UpgradeMigration.for.backfill
|
|
16
20
|
<% else -%>
|
|
17
21
|
raise 'Generate this migration with --mode copy' if table_exists?(:ruby_llm_v2_upgrades)
|
|
18
|
-
<% end -%>
|
|
19
22
|
verify_upgrade_in_progress
|
|
20
23
|
create_progress_table
|
|
21
24
|
backfill_required_defaults
|
|
22
|
-
<% if copy_mode? -%>
|
|
23
|
-
RubyLLM::Generators::UpgradeMigration.new.backfill
|
|
24
|
-
%w[message_content tool_results usages].each { |task| mark_completed(task) }
|
|
25
|
-
<% else -%>
|
|
26
25
|
backfill_message_content
|
|
27
26
|
backfill_tool_results
|
|
28
27
|
backfill_usages
|
|
29
|
-
<% end -%>
|
|
30
28
|
verify_backfills
|
|
29
|
+
<% end -%>
|
|
31
30
|
end
|
|
32
31
|
|
|
33
32
|
def down
|
|
@@ -69,18 +68,16 @@ class <%= backfill_migration_class_name %> < ActiveRecord::Migration<%= migratio
|
|
|
69
68
|
content = message_value(:content)
|
|
70
69
|
<% if postgresql? -%>
|
|
71
70
|
structured_content = "#{content_raw}::jsonb"
|
|
72
|
-
rendered_content = "#{content_raw}::text"
|
|
73
71
|
<% elsif mysql? -%>
|
|
74
72
|
structured_content = content_raw
|
|
75
|
-
rendered_content = "CAST(#{content_raw} AS CHAR)"
|
|
76
73
|
<% else -%>
|
|
77
74
|
structured_content = content_raw
|
|
78
|
-
rendered_content = content_raw
|
|
79
75
|
<% end -%>
|
|
76
|
+
rendered_content = RubyLLM::Generators::LegacyContentSQL.new(connection).render(content: content, raw: content_raw)
|
|
80
77
|
execute <<~SQL
|
|
81
78
|
UPDATE #{quote_table(:<%= message_table_name %>)} AS legacy_messages
|
|
82
79
|
SET #{quote_column(:raw_content)} = COALESCE(#{raw_content}, #{structured_content}),
|
|
83
|
-
#{quote_column(:content)} =
|
|
80
|
+
#{quote_column(:content)} = #{rendered_content}
|
|
84
81
|
WHERE #{range}
|
|
85
82
|
AND #{content_raw} IS NOT NULL
|
|
86
83
|
SQL
|
|
@@ -187,6 +184,10 @@ class <%= backfill_migration_class_name %> < ActiveRecord::Migration<%= migratio
|
|
|
187
184
|
end
|
|
188
185
|
|
|
189
186
|
def verify_backfills
|
|
187
|
+
<% if postgresql? -%>
|
|
188
|
+
tables = %i[<%= chat_table_name %> <%= message_table_name %> ruby_llm_models ruby_llm_tool_calls ruby_llm_usages]
|
|
189
|
+
with_upgrade_safety { execute "ANALYZE #{tables.map { |table| quote_table(table) }.join(', ')}" }
|
|
190
|
+
<% end -%>
|
|
190
191
|
verify_message_content
|
|
191
192
|
verify_tool_results
|
|
192
193
|
verify_usages
|
|
@@ -300,6 +301,7 @@ class <%= backfill_migration_class_name %> < ActiveRecord::Migration<%= migratio
|
|
|
300
301
|
|
|
301
302
|
def usage_candidate_sql(conditions, joins, range)
|
|
302
303
|
usages = quote_table(:ruby_llm_usages)
|
|
304
|
+
usage_range = range.gsub(message_value(connection.primary_key(:<%= message_table_name %>)), 'existing_usages.message_id')
|
|
303
305
|
<<~SQL
|
|
304
306
|
FROM #{quote_table(:<%= message_table_name %>)} legacy_messages
|
|
305
307
|
#{joins}
|
|
@@ -309,6 +311,7 @@ class <%= backfill_migration_class_name %> < ActiveRecord::Migration<%= migratio
|
|
|
309
311
|
SELECT 1
|
|
310
312
|
FROM #{usages} existing_usages
|
|
311
313
|
WHERE existing_usages.message_type = #{connection.quote('<%= message_model_name %>')}
|
|
314
|
+
AND (#{usage_range})
|
|
312
315
|
AND existing_usages.message_id = #{message_value(connection.primary_key(:<%= message_table_name %>))}
|
|
313
316
|
AND existing_usages.operation = 'chat'
|
|
314
317
|
AND existing_usages.status = 'succeeded'
|
|
@@ -11,15 +11,14 @@ class <%= cleanup_migration_class_name %> < ActiveRecord::Migration<%= migration
|
|
|
11
11
|
|
|
12
12
|
def up
|
|
13
13
|
<% if copy_mode? -%>
|
|
14
|
-
RubyLLM::Generators::UpgradeMigration.
|
|
15
|
-
RubyLLM::Generators::UpgradeMigration.
|
|
14
|
+
RubyLLM::Generators::UpgradeMigration.for.verify_settings(<%= copy_upgrade_settings.inspect %>)
|
|
15
|
+
RubyLLM::Generators::UpgradeMigration.for.verify_cleanup
|
|
16
16
|
<% else -%>
|
|
17
17
|
raise 'Generate cleanup with --mode copy for this database' if table_exists?(:ruby_llm_v2_upgrades)
|
|
18
18
|
<% end -%>
|
|
19
19
|
unless table_exists?(PROGRESS_TABLE)
|
|
20
20
|
<% if copy_mode? -%>
|
|
21
|
-
remove_legacy_message_columns
|
|
22
|
-
RubyLLM::Generators::UpgradeMigration.new.cleanup
|
|
21
|
+
RubyLLM::Generators::UpgradeMigration.for.cleanup { remove_legacy_message_columns }
|
|
23
22
|
return
|
|
24
23
|
<% else -%>
|
|
25
24
|
return unless legacy_columns.any?
|
|
@@ -31,9 +30,10 @@ class <%= cleanup_migration_class_name %> < ActiveRecord::Migration<%= migration
|
|
|
31
30
|
raise 'Run <%= finish_migration_class_name %> before removing the legacy message columns'
|
|
32
31
|
end
|
|
33
32
|
|
|
34
|
-
remove_legacy_message_columns
|
|
35
33
|
<% if copy_mode? -%>
|
|
36
|
-
RubyLLM::Generators::UpgradeMigration.
|
|
34
|
+
RubyLLM::Generators::UpgradeMigration.for.cleanup { remove_legacy_message_columns }
|
|
35
|
+
<% else -%>
|
|
36
|
+
remove_legacy_message_columns
|
|
37
37
|
<% end -%>
|
|
38
38
|
drop_table PROGRESS_TABLE
|
|
39
39
|
end
|
|
@@ -12,20 +12,26 @@ class <%= finish_migration_class_name %> < ActiveRecord::Migration<%= migration_
|
|
|
12
12
|
|
|
13
13
|
def up
|
|
14
14
|
<% if copy_mode? -%>
|
|
15
|
-
RubyLLM::Generators::UpgradeMigration.
|
|
16
|
-
RubyLLM::Generators::UpgradeMigration.
|
|
15
|
+
RubyLLM::Generators::UpgradeMigration.for.verify_settings(<%= copy_upgrade_settings.inspect %>)
|
|
16
|
+
RubyLLM::Generators::UpgradeMigration.for.finish do
|
|
17
|
+
verify_completed_backfills
|
|
18
|
+
enforce_required_defaults
|
|
19
|
+
relax_legacy_message_constraints
|
|
20
|
+
mark_finished
|
|
21
|
+
end
|
|
17
22
|
<% else -%>
|
|
18
23
|
raise 'Generate this migration with --mode copy' if table_exists?(:ruby_llm_v2_upgrades)
|
|
19
|
-
<% end -%>
|
|
20
24
|
verify_completed_backfills
|
|
25
|
+
<% if postgresql? -%>
|
|
26
|
+
tables = %i[<%= chat_table_name %> <%= message_table_name %> ruby_llm_models ruby_llm_tool_calls ruby_llm_usages]
|
|
27
|
+
with_upgrade_safety { execute "ANALYZE #{tables.map { |table| quote_table(table) }.join(', ')}" }
|
|
28
|
+
<% end -%>
|
|
21
29
|
verify_message_content
|
|
22
30
|
verify_tool_results
|
|
23
31
|
verify_usages
|
|
24
32
|
enforce_required_defaults
|
|
25
33
|
relax_legacy_message_constraints
|
|
26
34
|
mark_finished
|
|
27
|
-
<% if copy_mode? -%>
|
|
28
|
-
RubyLLM::Generators::UpgradeMigration.new.activate
|
|
29
35
|
<% end -%>
|
|
30
36
|
end
|
|
31
37
|
|
|
@@ -36,6 +42,17 @@ class <%= finish_migration_class_name %> < ActiveRecord::Migration<%= migration_
|
|
|
36
42
|
private
|
|
37
43
|
|
|
38
44
|
def relax_legacy_message_constraints
|
|
45
|
+
<% if copy_mode? -%>
|
|
46
|
+
model_column = :<%= v1_model_foreign_key %>
|
|
47
|
+
if column_definition(:<%= chat_table_name %>, model_column)&.null == false
|
|
48
|
+
with_upgrade_safety { change_column_null :<%= chat_table_name %>, model_column, true }
|
|
49
|
+
end
|
|
50
|
+
connection.foreign_keys(:<%= v1_tool_call_table_name %>).each do |key|
|
|
51
|
+
next unless key.column == '<%= message_foreign_key %>'
|
|
52
|
+
|
|
53
|
+
remove_foreign_key :<%= v1_tool_call_table_name %>, column: key.column
|
|
54
|
+
end
|
|
55
|
+
<% end -%>
|
|
39
56
|
%i[<%= v1_model_foreign_key %> <%= v1_tool_call_foreign_key %>].each do |column|
|
|
40
57
|
next unless column_definition(:<%= message_table_name %>, column)&.null == false
|
|
41
58
|
|
|
@@ -51,6 +68,15 @@ class <%= finish_migration_class_name %> < ActiveRecord::Migration<%= migration_
|
|
|
51
68
|
end
|
|
52
69
|
|
|
53
70
|
def enforce_required_defaults
|
|
71
|
+
{
|
|
72
|
+
<%= chat_table_name %>: {cancelled: false},
|
|
73
|
+
<%= message_table_name %>: {cache_until_here: false},
|
|
74
|
+
ruby_llm_tool_calls: {message_type: '<%= message_model_name %>'}
|
|
75
|
+
}.each do |table, attributes|
|
|
76
|
+
migration_record(table).where(attributes.transform_values { nil }).in_batches(of: 10_000) do |batch|
|
|
77
|
+
batch.update_all(attributes)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
54
80
|
<% if copy_mode? -%>
|
|
55
81
|
enforce_not_null(:<%= chat_table_name %>, :ruby_llm_model_id)
|
|
56
82
|
<% end -%>
|
|
@@ -9,16 +9,11 @@ class <%= prepare_migration_class_name %> < ActiveRecord::Migration<%= migration
|
|
|
9
9
|
|
|
10
10
|
def up
|
|
11
11
|
<% if copy_mode? -%>
|
|
12
|
-
RubyLLM::Generators::UpgradeMigration.
|
|
12
|
+
RubyLLM::Generators::UpgradeMigration.for.prepare(<%= copy_upgrade_settings.inspect %>) { prepare_schema }
|
|
13
13
|
<% else -%>
|
|
14
14
|
raise 'This database uses --mode copy' if table_exists?(:ruby_llm_v2_upgrades)
|
|
15
|
+
prepare_schema
|
|
15
16
|
<% end -%>
|
|
16
|
-
validate_upgrade
|
|
17
|
-
move_models
|
|
18
|
-
add_chat_and_message_columns
|
|
19
|
-
move_tool_calls
|
|
20
|
-
create_usages
|
|
21
|
-
create_batches
|
|
22
17
|
end
|
|
23
18
|
|
|
24
19
|
def down
|
|
@@ -27,6 +22,15 @@ class <%= prepare_migration_class_name %> < ActiveRecord::Migration<%= migration
|
|
|
27
22
|
|
|
28
23
|
private
|
|
29
24
|
|
|
25
|
+
def prepare_schema
|
|
26
|
+
validate_upgrade
|
|
27
|
+
move_models
|
|
28
|
+
add_chat_and_message_columns
|
|
29
|
+
move_tool_calls
|
|
30
|
+
create_usages
|
|
31
|
+
create_batches
|
|
32
|
+
end
|
|
33
|
+
|
|
30
34
|
def validate_upgrade
|
|
31
35
|
%i[<%= chat_table_name %> <%= message_table_name %>].each do |table|
|
|
32
36
|
raise "Expected #{table} to exist before upgrading RubyLLM" unless table_exists?(table)
|
|
@@ -247,8 +251,7 @@ class <%= prepare_migration_class_name %> < ActiveRecord::Migration<%= migration
|
|
|
247
251
|
unless column_exists?(table, :ruby_llm_model_id)
|
|
248
252
|
add_column table, :ruby_llm_model_id, reference_type_for(:ruby_llm_models)
|
|
249
253
|
end
|
|
250
|
-
RubyLLM::Generators::UpgradeMigration.
|
|
251
|
-
with_upgrade_safety { change_column_null table, column, true } unless column_definition(table, column).null
|
|
254
|
+
RubyLLM::Generators::UpgradeMigration.for.copy_model_references
|
|
252
255
|
<% else -%>
|
|
253
256
|
with_upgrade_safety { rename_column table, column, :ruby_llm_model_id } if column != :ruby_llm_model_id
|
|
254
257
|
<% end -%>
|
|
@@ -288,7 +291,9 @@ class <%= prepare_migration_class_name %> < ActiveRecord::Migration<%= migration
|
|
|
288
291
|
end
|
|
289
292
|
|
|
290
293
|
def ensure_boolean_column(table, column)
|
|
291
|
-
|
|
294
|
+
unless column_exists?(table, column)
|
|
295
|
+
with_upgrade_safety { add_column table, column, :boolean, default: false }
|
|
296
|
+
end
|
|
292
297
|
return if ActiveRecord::Type::Boolean.new.cast(column_definition(table, column).default) == false
|
|
293
298
|
|
|
294
299
|
with_upgrade_safety { change_column_default table, column, false }
|
|
@@ -296,9 +301,6 @@ class <%= prepare_migration_class_name %> < ActiveRecord::Migration<%= migration
|
|
|
296
301
|
|
|
297
302
|
def move_tool_calls
|
|
298
303
|
move_table(:<%= v1_tool_call_table_name %>, :ruby_llm_tool_calls)
|
|
299
|
-
<% if copy_mode? -%>
|
|
300
|
-
remove_column_foreign_keys(:<%= v1_tool_call_table_name %>, :<%= message_foreign_key %>)
|
|
301
|
-
<% end -%>
|
|
302
304
|
table = :ruby_llm_tool_calls
|
|
303
305
|
<% if postgresql? -%>
|
|
304
306
|
normalize_postgresql_json_columns(table, %i[arguments])
|
|
@@ -524,7 +526,7 @@ class <%= prepare_migration_class_name %> < ActiveRecord::Migration<%= migration
|
|
|
524
526
|
|
|
525
527
|
def move_table(source, target)
|
|
526
528
|
<% if copy_mode? -%>
|
|
527
|
-
with_upgrade_safety { RubyLLM::Generators::UpgradeMigration.
|
|
529
|
+
with_upgrade_safety { RubyLLM::Generators::UpgradeMigration.for.copy_table(source, target) }
|
|
528
530
|
<% else -%>
|
|
529
531
|
return if source == target || !table_exists?(source)
|
|
530
532
|
|