mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-03-28 17:42:48 +01:00
* feat: auto-upgrade telemetry schema * Ensure numeric metrics fallback to valid values * Format data processing numeric metric lookup
174 lines
6.2 KiB
Ruby
174 lines
6.2 KiB
Ruby
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
# frozen_string_literal: true
|
|
|
|
module PotatoMesh
|
|
module App
|
|
module Database
|
|
# Column definitions required for environment telemetry support. Each
|
|
# entry pairs the column name with the SQL type used when backfilling
|
|
# legacy databases that pre-date the extended telemetry schema.
|
|
TELEMETRY_COLUMN_DEFINITIONS = [
|
|
["gas_resistance", "REAL"],
|
|
["current", "REAL"],
|
|
["iaq", "INTEGER"],
|
|
["distance", "REAL"],
|
|
["lux", "REAL"],
|
|
["white_lux", "REAL"],
|
|
["ir_lux", "REAL"],
|
|
["uv_lux", "REAL"],
|
|
["wind_direction", "INTEGER"],
|
|
["wind_speed", "REAL"],
|
|
["weight", "REAL"],
|
|
["wind_gust", "REAL"],
|
|
["wind_lull", "REAL"],
|
|
["radiation", "REAL"],
|
|
["rainfall_1h", "REAL"],
|
|
["rainfall_24h", "REAL"],
|
|
["soil_moisture", "INTEGER"],
|
|
["soil_temperature", "REAL"],
|
|
].freeze
|
|
|
|
# Open a connection to the application database applying common pragmas.
|
|
#
|
|
# @param readonly [Boolean] whether to open the database in read-only mode.
|
|
# @return [SQLite3::Database] configured database handle.
|
|
def open_database(readonly: false)
|
|
SQLite3::Database.new(PotatoMesh::Config.db_path, readonly: readonly).tap do |db|
|
|
db.busy_timeout = PotatoMesh::Config.db_busy_timeout_ms
|
|
db.execute("PRAGMA foreign_keys = ON")
|
|
end
|
|
end
|
|
|
|
# Execute the provided block and retry when SQLite reports a busy error.
|
|
#
|
|
# @param max_retries [Integer] maximum number of retries when locked.
|
|
# @param base_delay [Float] incremental back-off delay between retries.
|
|
# @yield Executes the database operation.
|
|
# @return [Object] result of the block.
|
|
def with_busy_retry(
|
|
max_retries: PotatoMesh::Config.db_busy_max_retries,
|
|
base_delay: PotatoMesh::Config.db_busy_retry_delay
|
|
)
|
|
attempts = 0
|
|
begin
|
|
yield
|
|
rescue SQLite3::BusyException
|
|
attempts += 1
|
|
raise if attempts > max_retries
|
|
|
|
sleep(base_delay * attempts)
|
|
retry
|
|
end
|
|
end
|
|
|
|
# Determine whether the database schema has already been provisioned.
|
|
#
|
|
# @return [Boolean] true when all required tables exist.
|
|
def db_schema_present?
|
|
return false unless File.exist?(PotatoMesh::Config.db_path)
|
|
|
|
db = open_database(readonly: true)
|
|
required = %w[nodes messages positions telemetry neighbors instances]
|
|
tables =
|
|
db.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name IN ('nodes','messages','positions','telemetry','neighbors','instances')",
|
|
).flatten
|
|
(required - tables).empty?
|
|
rescue SQLite3::Exception
|
|
false
|
|
ensure
|
|
db&.close
|
|
end
|
|
|
|
# Create the database schema using the bundled SQL files.
|
|
#
|
|
# @return [void]
|
|
def init_db
|
|
FileUtils.mkdir_p(File.dirname(PotatoMesh::Config.db_path))
|
|
db = open_database
|
|
%w[nodes messages positions telemetry neighbors instances].each do |schema|
|
|
sql_file = File.expand_path("../../../../data/#{schema}.sql", __dir__)
|
|
db.execute_batch(File.read(sql_file))
|
|
end
|
|
ensure
|
|
db&.close
|
|
end
|
|
|
|
# Apply any schema migrations required for older installations.
|
|
#
|
|
# @return [void]
|
|
def ensure_schema_upgrades
|
|
db = open_database
|
|
node_columns = db.execute("PRAGMA table_info(nodes)").map { |row| row[1] }
|
|
unless node_columns.include?("precision_bits")
|
|
db.execute("ALTER TABLE nodes ADD COLUMN precision_bits INTEGER")
|
|
node_columns << "precision_bits"
|
|
end
|
|
|
|
unless node_columns.include?("lora_freq")
|
|
db.execute("ALTER TABLE nodes ADD COLUMN lora_freq INTEGER")
|
|
end
|
|
|
|
unless node_columns.include?("modem_preset")
|
|
db.execute("ALTER TABLE nodes ADD COLUMN modem_preset TEXT")
|
|
end
|
|
|
|
message_columns = db.execute("PRAGMA table_info(messages)").map { |row| row[1] }
|
|
|
|
unless message_columns.include?("lora_freq")
|
|
db.execute("ALTER TABLE messages ADD COLUMN lora_freq INTEGER")
|
|
end
|
|
|
|
unless message_columns.include?("modem_preset")
|
|
db.execute("ALTER TABLE messages ADD COLUMN modem_preset TEXT")
|
|
end
|
|
|
|
unless message_columns.include?("channel_name")
|
|
db.execute("ALTER TABLE messages ADD COLUMN channel_name TEXT")
|
|
end
|
|
|
|
tables = db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='instances'").flatten
|
|
if tables.empty?
|
|
sql_file = File.expand_path("../../../../data/instances.sql", __dir__)
|
|
db.execute_batch(File.read(sql_file))
|
|
end
|
|
|
|
telemetry_tables =
|
|
db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='telemetry'").flatten
|
|
if telemetry_tables.empty?
|
|
telemetry_schema = File.expand_path("../../../../data/telemetry.sql", __dir__)
|
|
db.execute_batch(File.read(telemetry_schema))
|
|
end
|
|
|
|
telemetry_columns = db.execute("PRAGMA table_info(telemetry)").map { |row| row[1] }
|
|
TELEMETRY_COLUMN_DEFINITIONS.each do |name, type|
|
|
next if telemetry_columns.include?(name)
|
|
|
|
db.execute("ALTER TABLE telemetry ADD COLUMN #{name} #{type}")
|
|
telemetry_columns << name
|
|
end
|
|
rescue SQLite3::SQLException, Errno::ENOENT => e
|
|
warn_log(
|
|
"Failed to apply schema upgrade",
|
|
context: "database.schema",
|
|
error_class: e.class.name,
|
|
error_message: e.message,
|
|
)
|
|
ensure
|
|
db&.close
|
|
end
|
|
end
|
|
end
|
|
end
|