Class: Kozeki::State

Inherits:
Object
  • Object
show all
Defined in:
lib/kozeki/state.rb

Defined Under Namespace

Classes: DuplicatedItemIdError, NotFound

Constant Summary collapse

EPOCH =
1

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path:) ⇒ State

Returns a new instance of State.

Parameters:

  • path (String, #to_path, nil)


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

#dbObject (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

Returns:

  • (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

#closeObject



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_epochObject



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

Parameters:

  • path (Array<String>)


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_namesObject



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_pendingObject



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_collectionObject



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_pathsObject



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

Parameters:

  • id (id)


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

Parameters:



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

Raises:



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

#transactionObject



232
233
234
# File 'lib/kozeki/state.rb', line 232

def transaction(...)
  db.transaction(...)
end