Class: Kozeki::State
- Inherits:
-
Object
- Object
- Kozeki::State
- Defined in:
- lib/kozeki/state.rb
Defined Under Namespace
Classes: DuplicatedItemIdError, NotFound
Constant Summary collapse
- EPOCH =
1
Instance Attribute Summary collapse
-
#db ⇒ Object
readonly
Returns the value of attribute db.
Class Method Summary collapse
Instance Method Summary collapse
- #build_exist? ⇒ Boolean
- #clear! ⇒ Object
- #close ⇒ Object
- #count_collection_records(collection) ⇒ Object
- #create_build(t = Time.now) ⇒ Object
- #current_epoch ⇒ Object
-
#ensure_schema! ⇒ Object
Ensure schema for the present version of Kozeki.
- #find_record!(id) ⇒ Object
- #find_record_by_path!(path) ⇒ Object
-
#initialize(path:) ⇒ State
constructor
A new instance of State.
- #list_collection_names ⇒ Object
- #list_collection_names_pending ⇒ Object
- #list_collection_names_with_prefix(*prefixes) ⇒ Object
- #list_collection_records(collection) ⇒ Object
- #list_item_ids_for_garbage_collection ⇒ Object
- #list_record_paths ⇒ Object
- #list_records_by_id(id) ⇒ Object
- #list_records_by_pending_build_action(action) ⇒ Object
- #mark_build_completed(id) ⇒ Object
- #mark_item_id_to_remove(id) ⇒ Object
-
#process_markers! ⇒ Object
Clear all markers and delete ‘remove’d rows.
- #save_record(record) ⇒ Object
- #set_record_collections_pending(record_id, collections) ⇒ Object
- #set_record_pending_build_action(record, pending_build_action) ⇒ Object
- #transaction ⇒ Object
Constructor Details
#initialize(path:) ⇒ State
Returns a new instance of State.
23 24 25 26 27 28 29 30 31 |
# File 'lib/kozeki/state.rb', line 23 def initialize(path:) @db = SQLite3::Database.new( path || ':memory:', { results_as_hash: true, strict: true, # Disable SQLITE_DBCONFIG_DQS_DDL, SQLITE_DBCONFIG_DQS_DML } ) end |
Instance Attribute Details
#db ⇒ Object (readonly)
Returns the value of attribute db.
33 34 35 |
# File 'lib/kozeki/state.rb', line 33 def db @db end |
Class Method Details
.open(path:) ⇒ Object
15 16 17 18 19 20 |
# File 'lib/kozeki/state.rb', line 15 def self.open(path:) FileUtils.mkdir_p File.dirname(path) if path state = new(path:) state.ensure_schema! state end |
Instance Method Details
#build_exist? ⇒ Boolean
44 45 46 |
# File 'lib/kozeki/state.rb', line 44 def build_exist? @db.execute(%{select * from "builds" where completed = 1 limit 1})[0] end |
#clear! ⇒ Object
35 36 37 38 39 40 41 42 |
# File 'lib/kozeki/state.rb', line 35 def clear! @db.execute_batch <<~SQL delete from "records"; delete from "collection_memberships"; delete from "item_ids"; delete from "builds"; SQL end |
#close ⇒ Object
236 237 238 |
# File 'lib/kozeki/state.rb', line 236 def close db.close end |
#count_collection_records(collection) ⇒ Object
222 223 224 225 226 227 228 229 230 |
# File 'lib/kozeki/state.rb', line 222 def count_collection_records(collection) @db.execute(<<~SQL, [collection])[0].fetch('cnt') select count(*) cnt from "collection_memberships" where "collection_memberships"."collection" = ? SQL end |
#create_build(t = Time.now) ⇒ Object
48 49 50 51 |
# File 'lib/kozeki/state.rb', line 48 def create_build(t = Time.now) @db.execute(%{insert into "builds" ("built_at") values (?)}, [t.to_i]) @db.last_insert_row_id end |
#current_epoch ⇒ Object
319 320 321 322 323 324 |
# File 'lib/kozeki/state.rb', line 319 def current_epoch epoch_tables = @db.execute("select * from sqlite_schema where type = 'table' and name = 'kozeki_schema_epoch'") return nil if epoch_tables.empty? epoch = @db.execute(%{select "epoch" from "kozeki_schema_epoch" order by "epoch" desc limit 1}) epoch&.dig(0, 'epoch') end |
#ensure_schema! ⇒ Object
Ensure schema for the present version of Kozeki. As a state behaves like a cache, all tables will be removed when version is different.
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 |
# File 'lib/kozeki/state.rb', line 242 def ensure_schema! return if current_epoch == EPOCH db.execute_batch <<~SQL drop table if exists "kozeki_schema_epoch"; create table kozeki_schema_epoch ( "epoch" integer not null ) strict; SQL db.execute_batch <<~SQL drop table if exists "records"; create table "records" ( path text not null unique, id text not null, timestamp integer not null, mtime integer not null, meta text not null, build text, pending_build_action text not null default 'none', id_was text ) strict; SQL # Non-unique index; during normal file operation we may see duplicated IDs while we process events one-by-one db.execute_batch <<~SQL drop index if exists "idx_records_id"; create index "idx_records_id" on "records" ("id"); SQL db.execute_batch <<~SQL drop index if exists "idx_records_pending"; create index "idx_records_pending" on "records" ("pending_build_action"); SQL db.execute_batch <<~SQL drop table if exists "item_ids"; create table "item_ids" ( id text unique not null, pending_build_action text not null default 'none' ) strict; SQL db.execute_batch <<~SQL drop index if exists "idx_item_ids_pending"; create index "idx_item_ids_pending" on "item_ids" ("pending_build_action"); SQL db.execute_batch <<~SQL drop table if exists "collection_memberships"; create table "collection_memberships" ( collection text not null, record_id text not null, pending_build_action text not null default 'none' ) strict; SQL db.execute_batch <<~SQL drop index if exists "idx_col_record"; create unique index "idx_col_record" on "collection_memberships" ("collection", "record_id"); SQL db.execute_batch <<~SQL drop index if exists "idx_col_pending"; create index "idx_col_pending" on "collection_memberships" ("pending_build_action", "collection"); SQL db.execute_batch <<~SQL drop table if exists "builds"; create table "builds" ( id integer primary key, built_at integer not null, completed integer not null default 0 ) strict; SQL db.execute(%{delete from "kozeki_schema_epoch"}) db.execute(%{insert into "kozeki_schema_epoch" values (?)}, [EPOCH]) nil end |
#find_record!(id) ⇒ Object
80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/kozeki/state.rb', line 80 def find_record!(id) rows = @db.execute(%{select * from "records" where "id" = ? and "pending_build_action" <> 'remove'}, [id]) case rows.size when 0 raise NotFound, "record not found for id=#{id.inspect}" when 1 Record.from_row(rows[0]) else raise DuplicatedItemIdError, "multiple records found for id=#{id.inspect}, resolve conflict first" end end |
#find_record_by_path!(path) ⇒ Object
71 72 73 74 75 76 77 78 |
# File 'lib/kozeki/state.rb', line 71 def find_record_by_path!(path) row = @db.execute(%{select * from "records" where "path" = ?}, [path.join('/')])[0] if row Record.from_row(row) else raise NotFound, "record not found for path=#{path.inspect}" end end |
#list_collection_names ⇒ Object
194 195 196 197 198 |
# File 'lib/kozeki/state.rb', line 194 def list_collection_names @db.execute(%{select distinct "collection" from "collection_memberships"}).map do |row| row.fetch('collection') end end |
#list_collection_names_pending ⇒ Object
188 189 190 191 192 |
# File 'lib/kozeki/state.rb', line 188 def list_collection_names_pending @db.execute(%{select distinct "collection" from "collection_memberships" where "pending_build_action" <> 'none'}).map do |row| row.fetch('collection') end end |
#list_collection_names_with_prefix(*prefixes) ⇒ Object
200 201 202 203 204 205 206 |
# File 'lib/kozeki/state.rb', line 200 def list_collection_names_with_prefix(*prefixes) return list_collection_names() if prefixes.empty? conditions = prefixes.map { %{"collection" glob '#{SQLite3::Database.quote(_1)}*'} } @db.execute(%{select distinct "collection" from "collection_memberships" where (#{conditions.join('or')})}).map do |row| row.fetch('collection') end end |
#list_collection_records(collection) ⇒ Object
209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/kozeki/state.rb', line 209 def list_collection_records(collection) @db.execute(<<~SQL, [collection]).map { Record.from_row(_1) } select "records".* from "collection_memberships" inner join "records" on "collection_memberships"."record_id" = "records"."id" where "collection_memberships"."collection" = ? and "collection_memberships"."pending_build_action" <> 'remove' and "records"."pending_build_action" <> 'remove' SQL end |
#list_item_ids_for_garbage_collection ⇒ Object
177 178 179 180 181 |
# File 'lib/kozeki/state.rb', line 177 def list_item_ids_for_garbage_collection @db.execute(%{select "id" from "item_ids" where "pending_build_action" = 'garbage_collection'}).map do |row| row.fetch('id') end end |
#list_record_paths ⇒ Object
102 103 104 105 |
# File 'lib/kozeki/state.rb', line 102 def list_record_paths rows = @db.execute(%{select "path" from "records"}) rows.map { _1.fetch('path').split('/') } # XXX: consolidate with Record logic end |
#list_records_by_id(id) ⇒ Object
97 98 99 100 |
# File 'lib/kozeki/state.rb', line 97 def list_records_by_id(id) rows = @db.execute(%{select * from "records" where "id" = ?}, [id.to_s]) rows.map { Record.from_row(_1) } end |
#list_records_by_pending_build_action(action) ⇒ Object
92 93 94 95 |
# File 'lib/kozeki/state.rb', line 92 def list_records_by_pending_build_action(action) rows = @db.execute(%{select * from "records" where "pending_build_action" = ?}, [action.to_s]) rows.map { Record.from_row(_1) } end |
#mark_build_completed(id) ⇒ Object
54 55 56 |
# File 'lib/kozeki/state.rb', line 54 def mark_build_completed(id) @db.execute(%{update "builds" set completed = 1 where id = ?}, [id]) end |
#mark_item_id_to_remove(id) ⇒ Object
183 184 185 186 |
# File 'lib/kozeki/state.rb', line 183 def mark_item_id_to_remove(id) @db.execute(%{update "item_ids" set "pending_build_action" = 'remove' where "id" = ?}, [id]) nil end |
#process_markers! ⇒ Object
Clear all markers and delete ‘remove’d rows.
59 60 61 62 63 64 65 66 67 68 |
# File 'lib/kozeki/state.rb', line 59 def process_markers! @db.execute_batch <<~SQL delete from "records" where "pending_build_action" = 'remove'; update "records" set "pending_build_action" = 'none', "id_was" = null where "pending_build_action" <> 'none'; delete from "collection_memberships" where "pending_build_action" = 'remove'; update "collection_memberships" set "pending_build_action" = 'none' where "pending_build_action" <> 'none'; delete from "item_ids" where "pending_build_action" = 'remove'; update "item_ids" set "pending_build_action" = 'none' where "pending_build_action" <> 'none'; SQL end |
#save_record(record) ⇒ Object
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/kozeki/state.rb', line 108 def save_record(record) new_row = @db.execute(<<~SQL, record.to_row)[0] insert into "records" ("path", "id", "timestamp", "mtime", "meta", "build", "pending_build_action") values (:path, :id, :timestamp, :mtime, :meta, :build, :pending_build_action) on conflict ("path") do update set "id" = excluded."id" , "timestamp" = excluded."timestamp" , "mtime" = excluded."mtime" , "meta" = excluded."meta" , "build" = excluded."build" , "pending_build_action" = excluded."pending_build_action" , "id_was" = "id" returning * SQL id_was = new_row['id_was'] @db.execute(<<~SQL, [record.id]) insert into "item_ids" ("id") values (?) on conflict ("id") do update set "pending_build_action" = 'none' SQL case id_was when record.id record when nil record else @db.execute(<<~SQL, [id_was]) insert into "item_ids" ("id") values (?) on conflict ("id") do update set "pending_build_action" = 'garbage_collection' SQL Record.from_row(new_row) end end |
#set_record_collections_pending(record_id, collections) ⇒ Object
164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/kozeki/state.rb', line 164 def set_record_collections_pending(record_id, collections) @db.execute(%{update "collection_memberships" set pending_build_action = 'remove' where record_id = ?}, record_id) return if collections.empty? @db.execute(<<~SQL, collections.map { [_1, record_id, 'update'] }) insert into "collection_memberships" ("collection", "record_id", "pending_build_action") values #{collections.map { '(?,?,?)' }.join(',')} on conflict ("collection", "record_id") do update set "pending_build_action" = excluded."pending_build_action" SQL end |
#set_record_pending_build_action(record, pending_build_action) ⇒ Object
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/kozeki/state.rb', line 146 def set_record_pending_build_action(record, pending_build_action) path = record.path @db.execute(<<~SQL, {path: record.path_row, pending_build_action: pending_build_action.to_s}) update "records" set "pending_build_action" = :pending_build_action where "path" = :path SQL raise NotFound, "record not found to update for path=#{path}" if @db.changes.zero? if pending_build_action == :remove @db.execute(<<~SQL, [record.id]) update "item_ids" set "pending_build_action" = 'garbage_collection' where "id" = :id SQL end nil end |
#transaction ⇒ Object
232 233 234 |
# File 'lib/kozeki/state.rb', line 232 def transaction(...) db.transaction(...) end |