Version 0.8.0: gefaltete Indizes markiert, Review abgeschlossen
index -x schreibt Offset-x in den Kopf der .i-Datei, search bisektiert in der Ordnung des Index. Beim Umstieg .h- und mit -x gebaute .i-Indizes neu bauen.
This commit is contained in:
+4
-1
@@ -5,7 +5,9 @@ Alle wichtigen Änderungen stehen hier. Das Format folgt
|
|||||||
folgen [Semantic Versioning](https://semver.org/lang/de/). Die Version steht in
|
folgen [Semantic Versioning](https://semver.org/lang/de/). Die Version steht in
|
||||||
`lib/rdb/version.rb`; `rdb --version` zeigt sie an.
|
`lib/rdb/version.rb`; `rdb --version` zeigt sie an.
|
||||||
|
|
||||||
## [Unveröffentlicht]
|
## [0.8.0] - 2026-10-30
|
||||||
|
|
||||||
|
**Beim Umstieg:** Hashindizes (`.h`) und mit `index -x` gebaute Indizes (`.i`) neu bauen.
|
||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
- `doc/handbuch.txt`: der Text des gedruckten Handbuchs „COHERENT /rdb Relational Database Manager“ (346 Seiten), per OCR aus dem Scan, mit PDF- und Buchseiten.
|
- `doc/handbuch.txt`: der Text des gedruckten Handbuchs „COHERENT /rdb Relational Database Manager“ (346 Seiten), per OCR aus dem Scan, mit PDF- und Buchseiten.
|
||||||
@@ -42,6 +44,7 @@ folgen [Semantic Versioning](https://semver.org/lang/de/). Die Version steht in
|
|||||||
- Tabellen, Indexdateien und `substitute` werden über eine temporäre Datei mit `rename` geschrieben; ein Abbruch lässt die alte Datei statt einer halben neuen.
|
- Tabellen, Indexdateien und `substitute` werden über eine temporäre Datei mit `rename` geschrieben; ein Abbruch lässt die alte Datei statt einer halben neuen.
|
||||||
- `index -mb` und `-mi` sortieren nach dem Schlüssel ohne umgebende Leerzeichen, so wie `search` vergleicht.
|
- `index -mb` und `-mi` sortieren nach dem Schlüssel ohne umgebende Leerzeichen, so wie `search` vergleicht.
|
||||||
- `sorttable -r` kehrte auch die Reihenfolge gleicher Schlüssel um; sie bleibt jetzt erhalten.
|
- `sorttable -r` kehrte auch die Reihenfolge gleicher Schlüssel um; sie bleibt jetzt erhalten.
|
||||||
|
- `index -x` vermerkt die Faltung im Kopf der `.i`-Datei (`Offset-x`). `search` bisektiert in der Ordnung des Index: ohne `-x` in einem gefalteten Index wurden Sätze übersehen, mit `-x` in einem ungefalteten ebenso (dort und bei `-mb` liest die Teilsuche jetzt alle Einträge). `append` hält einen gefalteten Index in seiner Ordnung.
|
||||||
- `jointable -n` wurde stillschweigend ignoriert. Wie in der Man-Page vergleicht es die Schlüssel jetzt als Zahlen (`01`, `1` und ` 1.0` passen zusammen).
|
- `jointable -n` wurde stillschweigend ignoriert. Wie in der Man-Page vergleicht es die Schlüssel jetzt als Zahlen (`01`, `1` und ` 1.0` passen zusammen).
|
||||||
- Spaltennamen, die `justify` mit Leerzeichen aufgefüllt hat, wurden nicht mehr gefunden: `column Name` lieferte eine leere Spalte, `index` brach ab. Leerraum um die Namen im Kopf zählt jetzt nicht mehr.
|
- Spaltennamen, die `justify` mit Leerzeichen aufgefüllt hat, wurden nicht mehr gefunden: `column Name` lieferte eine leere Spalte, `index` brach ab. Leerraum um die Namen im Kopf zählt jetzt nicht mehr.
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ sind nach dem Handbuch neu geschrieben.
|
|||||||
mise trust # einmal nach dem Clone: mise.toml freigeben
|
mise trust # einmal nach dem Clone: mise.toml freigeben
|
||||||
mise install # Ruby 3.4
|
mise install # Ruby 3.4
|
||||||
mise run install # legt die Symlinks row, column, total ... in bin/ an
|
mise run install # legt die Symlinks row, column, total ... in bin/ an
|
||||||
mise run test # 132 Tests und die Beispiele aus dem Handbuch
|
mise run test # 136 Tests und die Beispiele aus dem Handbuch
|
||||||
```
|
```
|
||||||
|
|
||||||
Ohne `mise trust` brechen in einem frisch geklonten Ordner alle `mise`-Befehle ab.
|
Ohne `mise trust` brechen in einem frisch geklonten Ordner alle `mise`-Befehle ab.
|
||||||
@@ -244,8 +244,9 @@ er nicht mehr passt (außer mit `-a`). Beide halten die Indizes aktuell.
|
|||||||
dem Lehrbuch in `demo/handbuch/` (`test/handbuch_test.rb`).
|
dem Lehrbuch in `demo/handbuch/` (`test/handbuch_test.rb`).
|
||||||
- Die Indexdateien sind dem Original nachgebaut, aber nicht gleich (Handbuch S. 103–108):
|
- Die Indexdateien sind dem Original nachgebaut, aber nicht gleich (Handbuch S. 103–108):
|
||||||
die Hashtabelle `.h` hat 2n statt 2n+1 Plätze (Hashfunktion FNV-1a), und in `.i` steht der
|
die Hashtabelle `.h` hat 2n statt 2n+1 Plätze (Hashfunktion FNV-1a), und in `.i` steht der
|
||||||
Schlüssel vor dem Offset statt dahinter. Alte /rdb-Indizes und `.h`-Dateien früherer
|
Schlüssel vor dem Offset statt dahinter. Ein mit `index -x` gebauter `.i`
|
||||||
Versionen dieses Nachbaus muss man neu bauen.
|
heißt in der zweiten Spalte `Offset-x`. Alte /rdb-Indizes sowie `.h`- und mit `-x` gebaute
|
||||||
|
`.i`-Dateien von Versionen vor 0.8.0 muss man neu bauen.
|
||||||
- Summen, Mittelwerte und /act-Beträge sind Dezimalzahlen (`Rational`), gerundet wird halb auf.
|
- Summen, Mittelwerte und /act-Beträge sind Dezimalzahlen (`Rational`), gerundet wird halb auf.
|
||||||
- Dateien, die kein UTF-8 sind, liest `rdb` als Latin-1 und schreibt sie als UTF-8. Fehler
|
- Dateien, die kein UTF-8 sind, liest `rdb` als Latin-1 und schreibt sie als UTF-8. Fehler
|
||||||
erscheinen als eine Zeile; mit `RDB_DEBUG=1` kommt der Ruby-Backtrace. Werte mit Tab oder
|
erscheinen als eine Zeile; mit `RDB_DEBUG=1` kommt der Ruby-Backtrace. Werte mit Tab oder
|
||||||
|
|||||||
+1
-1
@@ -880,7 +880,7 @@ Item Amount Cost Value Description
|
|||||||
## tabletom4 / tabletosed / tabletostruct were shown above
|
## tabletom4 / tabletosed / tabletostruct were shown above
|
||||||
## ===== Help and the terminal =====
|
## ===== Help and the terminal =====
|
||||||
$ rdb --version
|
$ rdb --version
|
||||||
rdb 0.7.0
|
rdb 0.8.0
|
||||||
$ howmany
|
$ howmany
|
||||||
136
|
136
|
||||||
$ whatis row
|
$ whatis row
|
||||||
|
|||||||
+5
-3
@@ -75,8 +75,9 @@ Man-Page) behoben. Bleibt so (Entscheidung 2026-10-30):
|
|||||||
- `1e3` ist keine Zahl: sonst würden Spalten mit Codes wie `2E5` numerisch.
|
- `1e3` ist keine Zahl: sonst würden Spalten mit Codes wie `2E5` numerisch.
|
||||||
- Eine Zeile aus lauter leeren Zellen gilt als gelöscht: so arbeitet `delete` im Format.
|
- Eine Zeile aus lauter leeren Zellen gilt als gelöscht: so arbeitet `delete` im Format.
|
||||||
|
|
||||||
Offen bei 8: `index -x` (gefaltet) hat in `search` ohne `-x` kein Gegenstück, weil die
|
Bei 8 ist auch die Faltung behoben: `index -x` schreibt `Offset-x` in den Kopf der `.i`-Datei,
|
||||||
`.i`-Datei die Faltung nicht vermerkt. Das bräuchte eine Markierung im Indexkopf.
|
`search` bisektiert in der Ordnung des Index und liest bei einer Teilsuche in einem
|
||||||
|
ungefalteten Index (und bei `-mb`) alle Einträge.
|
||||||
|
|
||||||
## Verbesserungspotential
|
## Verbesserungspotential
|
||||||
|
|
||||||
@@ -109,4 +110,5 @@ Offen bei 8: `index -x` (gefaltet) hat in `search` ohne `-x` kein Gegenstück, w
|
|||||||
## Empfohlene Reihenfolge
|
## Empfohlene Reihenfolge
|
||||||
|
|
||||||
Schwachstellen, Fehler und Verbesserungen 1–3 und 5–7 sind erledigt.
|
Schwachstellen, Fehler und Verbesserungen 1–3 und 5–7 sind erledigt.
|
||||||
Die awk-Brücke (4) und das Repo (8) bleiben auf Entscheidung des Autors so. Das Review ist damit abgeschlossen.
|
Die awk-Brücke (4) und das Repo (8) bleiben auf Entscheidung des Autors so. Das Review ist
|
||||||
|
damit abgeschlossen; alle Änderungen sind in Version 0.8.0.
|
||||||
|
|||||||
+27
-8
@@ -14,6 +14,9 @@ module Rdb
|
|||||||
ENTRY_WIDTH = 9
|
ENTRY_WIDTH = 9
|
||||||
SUFFIX = { 'h' => '.h', 'i' => '.i', 'r' => '.r' }.freeze
|
SUFFIX = { 'h' => '.h', 'i' => '.i', 'r' => '.r' }.freeze
|
||||||
FIXED_HEAD = "Offset\n------\n"
|
FIXED_HEAD = "Offset\n------\n"
|
||||||
|
# Second head column of an inverted index built with index -x: its keys are
|
||||||
|
# sorted without regard to case, for search -x (like look).
|
||||||
|
FOLDED = 'Offset-x'
|
||||||
|
|
||||||
Row = Struct.new(:from, :to, :line) do
|
Row = Struct.new(:from, :to, :line) do
|
||||||
def cells
|
def cells
|
||||||
@@ -119,7 +122,8 @@ module Rdb
|
|||||||
k = data.column(keycol)
|
k = data.column(keycol)
|
||||||
ents = rows.map { |r| [r.cells[k].to_s, r.from] }
|
ents = rows.map { |r| [r.cells[k].to_s, r.from] }
|
||||||
ents = ents.sort_by { |key, off| [fold ? key.strip.downcase : key.strip, off] }
|
ents = ents.sort_by { |key, off| [fold ? key.strip.downcase : key.strip, off] }
|
||||||
Rdb.write_file(path + '.i', "#{keycol}\tOffset\n#{'-' * keycol.size}\t------\n" + ents.map { |key, off| "#{key}\t#{off}\n" }.join)
|
off_col = fold ? FOLDED : 'Offset'
|
||||||
|
Rdb.write_file(path + '.i', Table.head_text([keycol, off_col]) + ents.map { |key, off| "#{key}\t#{off}\n" }.join)
|
||||||
when 'h'
|
when 'h'
|
||||||
k = data.column(keycol)
|
k = data.column(keycol)
|
||||||
slots = [rows.size * 2, 2].max
|
slots = [rows.size * 2, 2].max
|
||||||
@@ -151,6 +155,10 @@ module Rdb
|
|||||||
io.read(ENTRY_WIDTH).to_i
|
io.read(ENTRY_WIDTH).to_i
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def folded?(ipath)
|
||||||
|
File.open(ipath, 'rb') { |f| f.gets.to_s.chomp("\n").split("\t")[1] == FOLDED }
|
||||||
|
end
|
||||||
|
|
||||||
# Entries of an inverted index (.i) from the first key that is not below
|
# Entries of an inverted index (.i) from the first key that is not below
|
||||||
# target, found by bisecting on byte positions. Yields hashes like
|
# target, found by bisecting on byte positions. Yields hashes like
|
||||||
# read_inverted does.
|
# read_inverted does.
|
||||||
@@ -225,10 +233,13 @@ module Rdb
|
|||||||
when 'b'
|
when 'b'
|
||||||
rows = live_rows(data)
|
rows = live_rows(data)
|
||||||
k = idxs[0]
|
k = idxs[0]
|
||||||
lo = rows.bsearch_index { |r| (partial ? norm(r.cells[k], true)[0, keyvals[0].strip.size] : r.cells[k].strip) >= (partial ? norm(keyvals[0], true) : keyvals[0].strip) }
|
# The table is sorted by the key as it is (case counts), so a partial,
|
||||||
|
# case-blind search cannot bisect; it reads the rows in order.
|
||||||
|
lo = partial ? 0 : rows.bsearch_index { |r| r.cells[k].strip >= keyvals[0].strip }
|
||||||
if lo
|
if lo
|
||||||
rows[lo..].each do |r|
|
rows[lo..].each do |r|
|
||||||
break unless matches?(r.cells[k], keyvals[0], partial, exact)
|
break if !partial && r.cells[k].strip != keyvals[0].strip
|
||||||
|
next unless matches?(r.cells[k], keyvals[0], partial, exact)
|
||||||
|
|
||||||
hits << Hit.new(r, 0, 8)
|
hits << Hit.new(r, 0, 8)
|
||||||
break if first
|
break if first
|
||||||
@@ -242,10 +253,17 @@ module Rdb
|
|||||||
r = data.row_at(off) and hits << Hit.new(r, FIXED_HEAD.bytesize + ENTRY_WIDTH * (n - 1), FIXED_HEAD.bytesize + ENTRY_WIDTH * n)
|
r = data.row_at(off) and hits << Hit.new(r, FIXED_HEAD.bytesize + ENTRY_WIDTH * (n - 1), FIXED_HEAD.bytesize + ENTRY_WIDTH * n)
|
||||||
end
|
end
|
||||||
when 'i'
|
when 'i'
|
||||||
target = partial ? norm(keyvals[0], true) : keyvals[0].strip
|
# The index is in folded order (index -x) or not. Bisect in that order,
|
||||||
below = ->(key) { (partial ? norm(key, true)[0, keyvals[0].strip.size] : key.strip) < target }
|
# then keep the entries that match. A partial (case-blind) search in an
|
||||||
each_inverted_from(path + '.i', below) do |e|
|
# unfolded index has no order to use and reads the whole index.
|
||||||
break unless matches?(e[:key], keyvals[0], partial, exact)
|
ip = path + '.i'
|
||||||
|
fold = folded?(ip)
|
||||||
|
target = norm(keyvals[0], fold || partial)
|
||||||
|
cut = ->(key) { norm(key, fold || partial).then { |k| partial ? k[0, target.size] : k } }
|
||||||
|
below = fold || !partial ? ->(key) { cut.(key) < target } : ->(_key) { false }
|
||||||
|
each_inverted_from(ip, below) do |e|
|
||||||
|
break if (fold || !partial) && cut.(e[:key]) != target
|
||||||
|
next unless matches?(e[:key], keyvals[0], partial, exact)
|
||||||
|
|
||||||
r = data.row_at(e[:offset])
|
r = data.row_at(e[:offset])
|
||||||
hits << Hit.new(r, e[:from], e[:to]) if r
|
hits << Hit.new(r, e[:from], e[:to]) if r
|
||||||
@@ -318,7 +336,8 @@ module Rdb
|
|||||||
name = File.binread(ip).lines.first.split("\t")[0]
|
name = File.binread(ip).lines.first.split("\t")[0]
|
||||||
key = cells[data_head.index(name)].to_s
|
key = cells[data_head.index(name)].to_s
|
||||||
ents = read_inverted(ip)
|
ents = read_inverted(ip)
|
||||||
pos = ents.find { |e| e[:key].strip > key.strip }
|
fold = folded?(ip)
|
||||||
|
pos = ents.find { |e| norm(e[:key], fold) > norm(key, fold) }
|
||||||
line = "#{key}\t#{row_from}\n"
|
line = "#{key}\t#{row_from}\n"
|
||||||
if pos then splice(ip, pos[:from], pos[:from], line)
|
if pos then splice(ip, pos[:from], pos[:from], line)
|
||||||
else Rdb.write_file(ip, File.binread(ip) + line)
|
else Rdb.write_file(ip, File.binread(ip) + line)
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Rdb
|
module Rdb
|
||||||
VERSION = '0.7.0'
|
VERSION = '0.8.0'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -971,3 +971,43 @@ class JoinNumericTest < RdbCase
|
|||||||
assert_equal 3, data(rdb('jointable', '-n', '-a1', 'j1', 'j2')).size
|
assert_equal 3, data(rdb('jointable', '-n', '-a1', 'j1', 'j2')).size
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class FoldedIndexTest < RdbCase
|
||||||
|
def setup
|
||||||
|
super
|
||||||
|
write('fold', "K\tV\n-\t-\nb\t1\nApple\t2\nc\t3\napricot\t4\nB\t5\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
def keys(out)
|
||||||
|
data(out).map(&:first).sort
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_folded_index_is_marked_and_found_both_ways
|
||||||
|
rdb('index', '-mi', '-x', 'fold', 'K')
|
||||||
|
assert_equal "K\tOffset-x", read('fold.i').lines.first.chomp
|
||||||
|
assert_equal %w[Apple apricot], keys(rdb('search', '-mi', '-x', 'fold', 'K', stdin: "ap\n"))
|
||||||
|
assert_equal %w[c], keys(rdb('search', '-mi', 'fold', 'K', stdin: "c\n"))
|
||||||
|
assert_equal %w[B], keys(rdb('search', '-mi', 'fold', 'K', stdin: "B\n"))
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_partial_search_in_a_plain_index_finds_every_case
|
||||||
|
rdb('index', '-mi', 'fold', 'K')
|
||||||
|
assert_equal "K\tOffset", read('fold.i').lines.first.chomp
|
||||||
|
assert_equal %w[B b], keys(rdb('search', '-mi', '-x', 'fold', 'K', stdin: "b\n"))
|
||||||
|
assert_equal %w[Apple apricot], keys(rdb('search', '-mi', '-x', 'fold', 'K', stdin: "AP\n"))
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_append_keeps_a_folded_index_in_order
|
||||||
|
rdb('index', '-mi', '-x', 'fold', 'K')
|
||||||
|
rdb('append', '-mi', 'fold', 'K', stdin: "K\tV\n-\t-\nAzure\t6\n")
|
||||||
|
folded = read('fold.i').lines.drop(2).map { |l| l.split("\t").first.downcase }
|
||||||
|
assert_equal folded.sort, folded
|
||||||
|
assert_equal %w[Apple Azure apricot], keys(rdb('search', '-mi', '-x', 'fold', 'K', stdin: "a\n"))
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_partial_search_on_a_sorted_table
|
||||||
|
rdb('index', '-mb', 'fold', 'K')
|
||||||
|
assert_equal %w[B b], keys(rdb('search', '-mb', '-x', 'fold', 'K', stdin: "b\n"))
|
||||||
|
assert_equal %w[b], keys(rdb('search', '-mb', 'fold', 'K', stdin: "b\n"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user