diff options
| -rw-r--r-- | org/in_abstraction_artefacts.org | 43 | ||||
| -rw-r--r-- | org/out_ocda_sqlite_db.org | 143 | ||||
| -rw-r--r-- | src/sisudoc/ocda/abstraction/db_in.d | 43 | ||||
| -rw-r--r-- | src/sisudoc/outputs/io_out/sqlite_ocda_db.d | 143 |
4 files changed, 292 insertions, 80 deletions
diff --git a/org/in_abstraction_artefacts.org b/org/in_abstraction_artefacts.org index 9da6d32..c2b4c7a 100644 --- a/org/in_abstraction_artefacts.org +++ b/org/in_abstraction_artefacts.org @@ -659,16 +659,41 @@ template spineAbstractionDbRead() { default: return ""; } } - @trusted SSPdocument dbReadFile(string db_file) { + @trusted SSPdocument dbReadFile(string db_file, string _lang = "") { SSPdocument doc; if (!db_file.exists) { writeln("ERROR: no such file: ", db_file); return doc; } auto db = Database(db_file, SQLITE_OPEN_READONLY); + /+ ↓ which document in the file. A file holds one per language; with + no language named, take the only one, and say so if there is a + choice to be made rather than picking for the caller. + +/ + long _doc_id = -1; + { + string[] _langs; + foreach (row; db.execute("SELECT id, lang FROM documents ORDER BY id")) { + string _l = row["lang"].as!string; + _langs ~= _l; + if (_lang.length > 0 && _l == _lang) { _doc_id = row["id"].as!long; } + else if (_lang.length == 0 && _doc_id < 0) { _doc_id = row["id"].as!long; } + } + if (_doc_id < 0) { + writeln("ERROR: ", db_file, " holds no document in '", _lang, + "'; it has: ", _langs.join(" ")); + return doc; + } + if (_lang.length == 0 && _langs.length > 1) { + writeln("ERROR: ", db_file, " holds ", _langs.length, + " languages and none was named: ", _langs.join(" ")); + return doc; + } + } /+ ↓ the four header blocks, from the metadata table. the key prefix says which block a row belongs to, and insertion order is kept +/ - foreach (row; db.execute("SELECT key, value FROM metadata ORDER BY rowid")) { + foreach (row; db.execute("SELECT key, value FROM metadata WHERE doc_id = " ~ _doc_id.to!string + ~ " OR doc_id IS NULL ORDER BY rowid")) { string _k = row["key"].as!string; string _v = row["value"].as!string; if (_k.startsWith("make.")) { @@ -717,7 +742,7 @@ template spineAbstractionDbRead() { string[][long] _subtoc_by_id; foreach (r; db.execute( "SELECT object_id, name, bytes, sha256, width, height, missing" - ~ " FROM object_images ORDER BY object_id, seq") + ~ " FROM object_images WHERE object_id IN (SELECT id FROM objects WHERE doc_id = " ~ _doc_id.to!string ~ ") ORDER BY object_id, seq") ) { ST_file_name_hash_size_ _img; _img.fileName = r["name"].as!string; @@ -729,18 +754,19 @@ template spineAbstractionDbRead() { _images_by_id[r["object_id"].as!long] ~= _img; } foreach (r; db.execute( - "SELECT object_id, url FROM object_links ORDER BY object_id, seq") + "SELECT object_id, url FROM object_links WHERE object_id IN (SELECT id FROM objects WHERE doc_id = " ~ _doc_id.to!string ~ ") ORDER BY object_id, seq") ) { _links_by_id[r["object_id"].as!long] ~= r["url"].as!string; } foreach (r; db.execute( - "SELECT object_id, anchor FROM object_anchors ORDER BY object_id, seq") + "SELECT object_id, anchor FROM object_anchors WHERE object_id IN (SELECT id FROM objects WHERE doc_id = " ~ _doc_id.to!string ~ ") ORDER BY object_id, seq") ) { _anchors_by_id[r["object_id"].as!long] ~= r["anchor"].as!string; } foreach (r; db.execute( - "SELECT object_id, entry FROM object_subtoc ORDER BY object_id, seq") + "SELECT object_id, entry FROM object_subtoc WHERE object_id IN (SELECT id FROM objects WHERE doc_id = " ~ _doc_id.to!string ~ ") ORDER BY object_id, seq") ) { _subtoc_by_id[r["object_id"].as!long] ~= r["entry"].as!string; } /+ ↓ the objects, section by section, in the order they were written +/ string[] _sections; foreach (row; db.execute( - "SELECT section FROM objects GROUP BY section ORDER BY MIN(id)") + "SELECT section FROM objects WHERE doc_id = " ~ _doc_id.to!string + ~ " GROUP BY section ORDER BY MIN(id)") ) { _sections ~= row["section"].as!string; } @@ -758,7 +784,8 @@ template spineAbstractionDbRead() { +/ int[string] _col; foreach (row; db.execute( - "SELECT * FROM objects WHERE section = '" ~ section ~ "' ORDER BY seq") + "SELECT * FROM objects WHERE doc_id = " ~ _doc_id.to!string + ~ " AND section = '" ~ section ~ "' ORDER BY seq") ) { if (_col.length == 0) { foreach (_i; 0 .. row.length) { _col[row.columnName(_i)] = _i.to!int; } diff --git a/org/out_ocda_sqlite_db.org b/org/out_ocda_sqlite_db.org index 6916fa0..1c8a3c7 100644 --- a/org/out_ocda_sqlite_db.org +++ b/org/out_ocda_sqlite_db.org @@ -98,9 +98,29 @@ template spineAbstractionDb() { db.run("PRAGMA synchronous=OFF"); db.run(" - CREATE TABLE metadata ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL + -- documents: a row per language of the document this database holds. + -- One today, since a database is written per language; the table is + -- what lets one hold a document's whole set, which is what it is for. + -- Everything a reader needs to tell one language's rows from another's + -- keys on documents.id, called doc_id wherever it is referred to. + CREATE TABLE IF NOT EXISTS documents ( + id INTEGER PRIMARY KEY, + lang TEXT NOT NULL, + source_filename TEXT, + ssp_digest TEXT, + UNIQUE(lang) + ); + -- metadata: the document's own header, a row per property. + -- Keyed on (doc_id, key) and not on key alone: every language has a + -- title and a creator, and a key-only primary key refuses the second + -- one. schema.name and schema.version describe the file rather than a + -- document in it, so they are written with doc_id null and are the + -- only rows that are. + CREATE TABLE IF NOT EXISTS metadata ( + doc_id INTEGER REFERENCES documents(id), + key TEXT NOT NULL, + value TEXT NOT NULL, + UNIQUE(doc_id, key) ); -- objects: one row per abstraction object, in document order. -- the fixed width arrays (ancestors, dom status, children, heading @@ -108,8 +128,9 @@ template spineAbstractionDb() { -- json_extract, json_each and json_array_length can reach inside them; -- the open ended lists (images, links, anchor tags, subtoc) are rows -- in their own tables, not columns - CREATE TABLE objects ( + CREATE TABLE IF NOT EXISTS objects ( id INTEGER PRIMARY KEY, + doc_id INTEGER NOT NULL REFERENCES documents(id), section TEXT NOT NULL, seq INTEGER NOT NULL, ocn INTEGER DEFAULT 0, @@ -167,14 +188,14 @@ template spineAbstractionDb() { table_header INTEGER, code_linenumbers INTEGER DEFAULT 0, text TEXT, - UNIQUE(section, seq) + UNIQUE(doc_id, section, seq) ); - CREATE INDEX idx_objects_section ON objects(section); - CREATE INDEX idx_objects_ocn ON objects(ocn); - CREATE INDEX idx_objects_parent ON objects(parent_ocn); - CREATE INDEX idx_objects_is_a ON objects(is_a); - CREATE INDEX idx_objects_heading ON objects(heading_level) + CREATE INDEX IF NOT EXISTS idx_objects_section ON objects(doc_id, section); + CREATE INDEX IF NOT EXISTS idx_objects_ocn ON objects(ocn); + CREATE INDEX IF NOT EXISTS idx_objects_parent ON objects(parent_ocn); + CREATE INDEX IF NOT EXISTS idx_objects_is_a ON objects(is_a); + CREATE INDEX IF NOT EXISTS idx_objects_heading ON objects(heading_level) WHERE heading_level IS NOT NULL; -- files: what this database carries with it, so that a reader needs @@ -182,7 +203,7 @@ template spineAbstractionDb() { -- output writers must have in order to produce a document. bytes are -- stored exactly as read, so sha256 matches the digest the abstraction -- was built with - CREATE TABLE files ( + CREATE TABLE IF NOT EXISTS files ( id INTEGER PRIMARY KEY, role TEXT NOT NULL, name TEXT NOT NULL, @@ -195,7 +216,7 @@ template spineAbstractionDb() { ); -- the open ended per object lists, one row each, in order - CREATE TABLE object_images ( + CREATE TABLE IF NOT EXISTS object_images ( object_id INTEGER NOT NULL REFERENCES objects(id), seq INTEGER NOT NULL, name TEXT NOT NULL, @@ -206,43 +227,52 @@ template spineAbstractionDb() { missing INTEGER DEFAULT 0, PRIMARY KEY(object_id, seq) ); - CREATE TABLE object_links ( + CREATE TABLE IF NOT EXISTS object_links ( object_id INTEGER NOT NULL REFERENCES objects(id), seq INTEGER NOT NULL, url TEXT NOT NULL, PRIMARY KEY(object_id, seq) ); - CREATE TABLE object_anchors ( + CREATE TABLE IF NOT EXISTS object_anchors ( object_id INTEGER NOT NULL REFERENCES objects(id), seq INTEGER NOT NULL, anchor TEXT NOT NULL, PRIMARY KEY(object_id, seq) ); - CREATE TABLE object_subtoc ( + CREATE TABLE IF NOT EXISTS object_subtoc ( object_id INTEGER NOT NULL REFERENCES objects(id), seq INTEGER NOT NULL, entry TEXT NOT NULL, PRIMARY KEY(object_id, seq) ); - CREATE INDEX idx_files_role ON files(role); - CREATE INDEX idx_object_images_name ON object_images(name); + CREATE INDEX IF NOT EXISTS idx_files_role ON files(role); + CREATE INDEX IF NOT EXISTS idx_object_images_name ON object_images(name); -- full text over the object text, external content so the text is not -- stored twice. per document: this is for exploring one abstraction, -- the collection wide search database is a different thing - CREATE VIRTUAL TABLE objects_fts USING fts5( + CREATE VIRTUAL TABLE IF NOT EXISTS objects_fts USING fts5( text, content='objects', content_rowid='id' ); -- the shapes worth asking for, named - CREATE VIEW outline AS - SELECT section, seq, ocn, heading_level, heading_lev_collapsed, - last_descendant_ocn, identifier, text - FROM objects WHERE is_a = 'heading' ORDER BY id; - CREATE VIEW citable AS - SELECT * FROM objects WHERE ocn > 0 ORDER BY ocn; - CREATE VIEW document_files AS + -- both name the language, and order by it first. A view over a file + -- that may hold several languages and does not say which would read + -- as one document and be several: the language is in the projection + -- so that what comes back says what it is, and a reader wanting one + -- language adds WHERE lang = '..' rather than knowing to. + CREATE VIEW IF NOT EXISTS outline AS + SELECT d.lang AS lang, o.doc_id, o.section, o.seq, o.ocn, + o.heading_level, o.heading_lev_collapsed, + o.last_descendant_ocn, o.identifier, o.text + FROM objects o JOIN documents d ON d.id = o.doc_id + WHERE o.is_a = 'heading' ORDER BY o.doc_id, o.id; + CREATE VIEW IF NOT EXISTS citable AS + SELECT d.lang AS lang, o.* + FROM objects o JOIN documents d ON d.id = o.doc_id + WHERE o.ocn > 0 ORDER BY o.doc_id, o.ocn; + CREATE VIEW IF NOT EXISTS document_files AS SELECT role, name, bytes, sha256, width, height FROM files ORDER BY role, name; "); @@ -255,18 +285,51 @@ template spineAbstractionDb() { +/ db.run("BEGIN TRANSACTION"); + /+ ↓ the document this call is writing: one row in documents, and its id + is the doc_id every row of this language then carries. Written with + OR REPLACE on the language so that re-writing a language replaces + its row rather than refusing it. + +/ + long _doc_id; + { + auto doc_stmt = db.prepare( + "INSERT OR REPLACE INTO documents (lang, source_filename, ssp_digest)" + ~ " VALUES (:lang, :fn, :dig)" + ); + doc_stmt.bind(":lang", doc_matters.src.language); + doc_stmt.bind(":fn", ssp_doc.source); + doc_stmt.bind(":dig", ssp_doc.ssp_digest); + doc_stmt.execute(); + doc_stmt.finalize(); + _doc_id = db.lastInsertRowid; + } + auto meta_stmt = db.prepare( - "INSERT INTO metadata (key, value) VALUES (:key, :value)" + "INSERT INTO metadata (doc_id, key, value) VALUES (:doc_id, :key, :value)" ); void insertMeta(string key, string value) { if (value.length > 0) { + meta_stmt.bind(":doc_id", _doc_id); meta_stmt.bind(":key", key); meta_stmt.bind(":value", value); meta_stmt.execute(); meta_stmt.reset(); } } + /+ ↓ what the file is rather than what a document in it is, so these two + carry no doc_id and are the only rows that do not + +/ + void insertFileMeta(string key, string value) { + auto _s = db.prepare( + "INSERT OR REPLACE INTO metadata (doc_id, key, value)" + ~ " VALUES (NULL, :key, :value)" + ); + _s.bind(":key", key); + _s.bind(":value", value); + _s.execute(); + _s.finalize(); + } void insertBlock(string prefix, string[] keys_in_order, string[string] kv) { foreach (k; keys_in_order) { if (k in kv) { insertMeta(prefix ~ k, kv[k]); } @@ -277,8 +340,8 @@ template spineAbstractionDb() { version is the .ssp's own: the two serialisations are one format and move together +/ - insertMeta("schema.name", "sisu-abstraction-db"); - insertMeta("schema.version", ssp_format_version); + insertFileMeta("schema.name", "sisu-abstraction-db"); + insertFileMeta("schema.version", ssp_format_version); insertMeta("source.filename", ssp_doc.source); insertBlock("source.", ssp_doc.source_info_order, ssp_doc.source_info); /+ ↓ the digest of the .ssp this database was built from. a file cannot @@ -297,7 +360,7 @@ template spineAbstractionDb() { /+ ↓ populate objects +/ auto obj_stmt = db.prepare( "INSERT INTO objects (" - ~ "section, seq, ocn, is_a, is_of_part, is_of_section, is_of_type," + ~ "doc_id, section, seq, ocn, is_a, is_of_part, is_of_section, is_of_type," ~ "heading_level, heading_lev_collapsed, identifier," ~ "parent_ocn, parent_lev, last_descendant_ocn," ~ "children, ancestors, ancestors_collapsed," @@ -312,7 +375,7 @@ template spineAbstractionDb() { ~ "table_cols, table_widths, table_aligns, table_header," ~ "code_linenumbers, text" ~ ") VALUES (" - ~ ":section, :seq, :ocn, :is_a, :is_of_part, :is_of_section, :is_of_type," + ~ ":doc_id, :section, :seq, :ocn, :is_a, :is_of_part, :is_of_section, :is_of_type," ~ ":heading_level, :heading_lev_collapsed, :identifier," ~ ":parent_ocn, :parent_lev, :last_descendant_ocn," ~ ":children, :ancestors, :ancestors_collapsed," @@ -357,6 +420,7 @@ template spineAbstractionDb() { if (section_objs.length == 0) continue; foreach (seq, obj; section_objs) { + obj_stmt.bind(":doc_id", _doc_id); obj_stmt.bind(":section", section); obj_stmt.bind(":seq", cast(int) seq); obj_stmt.bind(":ocn", obj.metainfo.ocn); @@ -630,8 +694,23 @@ template spineAbstractionDb() { } /+ ↓ build the full text index from the rows just written +/ - db.run("INSERT INTO objects_fts(rowid, text)" - ~ " SELECT id, text FROM objects WHERE text IS NOT NULL"); + /+ ↓ scoped to this document, not the whole table. + (objects_fts is external content over objects.id, so re-running this + for a second language would re-insert every row the first language + put in: duplicate rowids, which fts5 does not refuse and which give + duplicated and wrong results. Nothing in the test suite queries the + index, so this one would not have been caught.) + +/ + { + auto _fts = db.prepare( + "INSERT INTO objects_fts(rowid, text)" + ~ " SELECT id, text FROM objects" + ~ " WHERE doc_id = :doc_id AND text IS NOT NULL" + ); + _fts.bind(":doc_id", _doc_id); + _fts.execute(); + _fts.finalize(); + } db.run("COMMIT TRANSACTION"); } diff --git a/src/sisudoc/ocda/abstraction/db_in.d b/src/sisudoc/ocda/abstraction/db_in.d index 6e90820..a8a216c 100644 --- a/src/sisudoc/ocda/abstraction/db_in.d +++ b/src/sisudoc/ocda/abstraction/db_in.d @@ -129,16 +129,41 @@ template spineAbstractionDbRead() { default: return ""; } } - @trusted SSPdocument dbReadFile(string db_file) { + @trusted SSPdocument dbReadFile(string db_file, string _lang = "") { SSPdocument doc; if (!db_file.exists) { writeln("ERROR: no such file: ", db_file); return doc; } auto db = Database(db_file, SQLITE_OPEN_READONLY); + /+ ↓ which document in the file. A file holds one per language; with + no language named, take the only one, and say so if there is a + choice to be made rather than picking for the caller. + +/ + long _doc_id = -1; + { + string[] _langs; + foreach (row; db.execute("SELECT id, lang FROM documents ORDER BY id")) { + string _l = row["lang"].as!string; + _langs ~= _l; + if (_lang.length > 0 && _l == _lang) { _doc_id = row["id"].as!long; } + else if (_lang.length == 0 && _doc_id < 0) { _doc_id = row["id"].as!long; } + } + if (_doc_id < 0) { + writeln("ERROR: ", db_file, " holds no document in '", _lang, + "'; it has: ", _langs.join(" ")); + return doc; + } + if (_lang.length == 0 && _langs.length > 1) { + writeln("ERROR: ", db_file, " holds ", _langs.length, + " languages and none was named: ", _langs.join(" ")); + return doc; + } + } /+ ↓ the four header blocks, from the metadata table. the key prefix says which block a row belongs to, and insertion order is kept +/ - foreach (row; db.execute("SELECT key, value FROM metadata ORDER BY rowid")) { + foreach (row; db.execute("SELECT key, value FROM metadata WHERE doc_id = " ~ _doc_id.to!string + ~ " OR doc_id IS NULL ORDER BY rowid")) { string _k = row["key"].as!string; string _v = row["value"].as!string; if (_k.startsWith("make.")) { @@ -187,7 +212,7 @@ template spineAbstractionDbRead() { string[][long] _subtoc_by_id; foreach (r; db.execute( "SELECT object_id, name, bytes, sha256, width, height, missing" - ~ " FROM object_images ORDER BY object_id, seq") + ~ " FROM object_images WHERE object_id IN (SELECT id FROM objects WHERE doc_id = " ~ _doc_id.to!string ~ ") ORDER BY object_id, seq") ) { ST_file_name_hash_size_ _img; _img.fileName = r["name"].as!string; @@ -199,18 +224,19 @@ template spineAbstractionDbRead() { _images_by_id[r["object_id"].as!long] ~= _img; } foreach (r; db.execute( - "SELECT object_id, url FROM object_links ORDER BY object_id, seq") + "SELECT object_id, url FROM object_links WHERE object_id IN (SELECT id FROM objects WHERE doc_id = " ~ _doc_id.to!string ~ ") ORDER BY object_id, seq") ) { _links_by_id[r["object_id"].as!long] ~= r["url"].as!string; } foreach (r; db.execute( - "SELECT object_id, anchor FROM object_anchors ORDER BY object_id, seq") + "SELECT object_id, anchor FROM object_anchors WHERE object_id IN (SELECT id FROM objects WHERE doc_id = " ~ _doc_id.to!string ~ ") ORDER BY object_id, seq") ) { _anchors_by_id[r["object_id"].as!long] ~= r["anchor"].as!string; } foreach (r; db.execute( - "SELECT object_id, entry FROM object_subtoc ORDER BY object_id, seq") + "SELECT object_id, entry FROM object_subtoc WHERE object_id IN (SELECT id FROM objects WHERE doc_id = " ~ _doc_id.to!string ~ ") ORDER BY object_id, seq") ) { _subtoc_by_id[r["object_id"].as!long] ~= r["entry"].as!string; } /+ ↓ the objects, section by section, in the order they were written +/ string[] _sections; foreach (row; db.execute( - "SELECT section FROM objects GROUP BY section ORDER BY MIN(id)") + "SELECT section FROM objects WHERE doc_id = " ~ _doc_id.to!string + ~ " GROUP BY section ORDER BY MIN(id)") ) { _sections ~= row["section"].as!string; } @@ -228,7 +254,8 @@ template spineAbstractionDbRead() { +/ int[string] _col; foreach (row; db.execute( - "SELECT * FROM objects WHERE section = '" ~ section ~ "' ORDER BY seq") + "SELECT * FROM objects WHERE doc_id = " ~ _doc_id.to!string + ~ " AND section = '" ~ section ~ "' ORDER BY seq") ) { if (_col.length == 0) { foreach (_i; 0 .. row.length) { _col[row.columnName(_i)] = _i.to!int; } diff --git a/src/sisudoc/outputs/io_out/sqlite_ocda_db.d b/src/sisudoc/outputs/io_out/sqlite_ocda_db.d index 79be05e..d94fa26 100644 --- a/src/sisudoc/outputs/io_out/sqlite_ocda_db.d +++ b/src/sisudoc/outputs/io_out/sqlite_ocda_db.d @@ -116,9 +116,29 @@ template spineAbstractionDb() { db.run("PRAGMA synchronous=OFF"); db.run(" - CREATE TABLE metadata ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL + -- documents: a row per language of the document this database holds. + -- One today, since a database is written per language; the table is + -- what lets one hold a document's whole set, which is what it is for. + -- Everything a reader needs to tell one language's rows from another's + -- keys on documents.id, called doc_id wherever it is referred to. + CREATE TABLE IF NOT EXISTS documents ( + id INTEGER PRIMARY KEY, + lang TEXT NOT NULL, + source_filename TEXT, + ssp_digest TEXT, + UNIQUE(lang) + ); + -- metadata: the document's own header, a row per property. + -- Keyed on (doc_id, key) and not on key alone: every language has a + -- title and a creator, and a key-only primary key refuses the second + -- one. schema.name and schema.version describe the file rather than a + -- document in it, so they are written with doc_id null and are the + -- only rows that are. + CREATE TABLE IF NOT EXISTS metadata ( + doc_id INTEGER REFERENCES documents(id), + key TEXT NOT NULL, + value TEXT NOT NULL, + UNIQUE(doc_id, key) ); -- objects: one row per abstraction object, in document order. -- the fixed width arrays (ancestors, dom status, children, heading @@ -126,8 +146,9 @@ template spineAbstractionDb() { -- json_extract, json_each and json_array_length can reach inside them; -- the open ended lists (images, links, anchor tags, subtoc) are rows -- in their own tables, not columns - CREATE TABLE objects ( + CREATE TABLE IF NOT EXISTS objects ( id INTEGER PRIMARY KEY, + doc_id INTEGER NOT NULL REFERENCES documents(id), section TEXT NOT NULL, seq INTEGER NOT NULL, ocn INTEGER DEFAULT 0, @@ -185,14 +206,14 @@ template spineAbstractionDb() { table_header INTEGER, code_linenumbers INTEGER DEFAULT 0, text TEXT, - UNIQUE(section, seq) + UNIQUE(doc_id, section, seq) ); - CREATE INDEX idx_objects_section ON objects(section); - CREATE INDEX idx_objects_ocn ON objects(ocn); - CREATE INDEX idx_objects_parent ON objects(parent_ocn); - CREATE INDEX idx_objects_is_a ON objects(is_a); - CREATE INDEX idx_objects_heading ON objects(heading_level) + CREATE INDEX IF NOT EXISTS idx_objects_section ON objects(doc_id, section); + CREATE INDEX IF NOT EXISTS idx_objects_ocn ON objects(ocn); + CREATE INDEX IF NOT EXISTS idx_objects_parent ON objects(parent_ocn); + CREATE INDEX IF NOT EXISTS idx_objects_is_a ON objects(is_a); + CREATE INDEX IF NOT EXISTS idx_objects_heading ON objects(heading_level) WHERE heading_level IS NOT NULL; -- files: what this database carries with it, so that a reader needs @@ -200,7 +221,7 @@ template spineAbstractionDb() { -- output writers must have in order to produce a document. bytes are -- stored exactly as read, so sha256 matches the digest the abstraction -- was built with - CREATE TABLE files ( + CREATE TABLE IF NOT EXISTS files ( id INTEGER PRIMARY KEY, role TEXT NOT NULL, name TEXT NOT NULL, @@ -213,7 +234,7 @@ template spineAbstractionDb() { ); -- the open ended per object lists, one row each, in order - CREATE TABLE object_images ( + CREATE TABLE IF NOT EXISTS object_images ( object_id INTEGER NOT NULL REFERENCES objects(id), seq INTEGER NOT NULL, name TEXT NOT NULL, @@ -224,43 +245,52 @@ template spineAbstractionDb() { missing INTEGER DEFAULT 0, PRIMARY KEY(object_id, seq) ); - CREATE TABLE object_links ( + CREATE TABLE IF NOT EXISTS object_links ( object_id INTEGER NOT NULL REFERENCES objects(id), seq INTEGER NOT NULL, url TEXT NOT NULL, PRIMARY KEY(object_id, seq) ); - CREATE TABLE object_anchors ( + CREATE TABLE IF NOT EXISTS object_anchors ( object_id INTEGER NOT NULL REFERENCES objects(id), seq INTEGER NOT NULL, anchor TEXT NOT NULL, PRIMARY KEY(object_id, seq) ); - CREATE TABLE object_subtoc ( + CREATE TABLE IF NOT EXISTS object_subtoc ( object_id INTEGER NOT NULL REFERENCES objects(id), seq INTEGER NOT NULL, entry TEXT NOT NULL, PRIMARY KEY(object_id, seq) ); - CREATE INDEX idx_files_role ON files(role); - CREATE INDEX idx_object_images_name ON object_images(name); + CREATE INDEX IF NOT EXISTS idx_files_role ON files(role); + CREATE INDEX IF NOT EXISTS idx_object_images_name ON object_images(name); -- full text over the object text, external content so the text is not -- stored twice. per document: this is for exploring one abstraction, -- the collection wide search database is a different thing - CREATE VIRTUAL TABLE objects_fts USING fts5( + CREATE VIRTUAL TABLE IF NOT EXISTS objects_fts USING fts5( text, content='objects', content_rowid='id' ); -- the shapes worth asking for, named - CREATE VIEW outline AS - SELECT section, seq, ocn, heading_level, heading_lev_collapsed, - last_descendant_ocn, identifier, text - FROM objects WHERE is_a = 'heading' ORDER BY id; - CREATE VIEW citable AS - SELECT * FROM objects WHERE ocn > 0 ORDER BY ocn; - CREATE VIEW document_files AS + -- both name the language, and order by it first. A view over a file + -- that may hold several languages and does not say which would read + -- as one document and be several: the language is in the projection + -- so that what comes back says what it is, and a reader wanting one + -- language adds WHERE lang = '..' rather than knowing to. + CREATE VIEW IF NOT EXISTS outline AS + SELECT d.lang AS lang, o.doc_id, o.section, o.seq, o.ocn, + o.heading_level, o.heading_lev_collapsed, + o.last_descendant_ocn, o.identifier, o.text + FROM objects o JOIN documents d ON d.id = o.doc_id + WHERE o.is_a = 'heading' ORDER BY o.doc_id, o.id; + CREATE VIEW IF NOT EXISTS citable AS + SELECT d.lang AS lang, o.* + FROM objects o JOIN documents d ON d.id = o.doc_id + WHERE o.ocn > 0 ORDER BY o.doc_id, o.ocn; + CREATE VIEW IF NOT EXISTS document_files AS SELECT role, name, bytes, sha256, width, height FROM files ORDER BY role, name; "); @@ -273,18 +303,51 @@ template spineAbstractionDb() { +/ db.run("BEGIN TRANSACTION"); + /+ ↓ the document this call is writing: one row in documents, and its id + is the doc_id every row of this language then carries. Written with + OR REPLACE on the language so that re-writing a language replaces + its row rather than refusing it. + +/ + long _doc_id; + { + auto doc_stmt = db.prepare( + "INSERT OR REPLACE INTO documents (lang, source_filename, ssp_digest)" + ~ " VALUES (:lang, :fn, :dig)" + ); + doc_stmt.bind(":lang", doc_matters.src.language); + doc_stmt.bind(":fn", ssp_doc.source); + doc_stmt.bind(":dig", ssp_doc.ssp_digest); + doc_stmt.execute(); + doc_stmt.finalize(); + _doc_id = db.lastInsertRowid; + } + auto meta_stmt = db.prepare( - "INSERT INTO metadata (key, value) VALUES (:key, :value)" + "INSERT INTO metadata (doc_id, key, value) VALUES (:doc_id, :key, :value)" ); void insertMeta(string key, string value) { if (value.length > 0) { + meta_stmt.bind(":doc_id", _doc_id); meta_stmt.bind(":key", key); meta_stmt.bind(":value", value); meta_stmt.execute(); meta_stmt.reset(); } } + /+ ↓ what the file is rather than what a document in it is, so these two + carry no doc_id and are the only rows that do not + +/ + void insertFileMeta(string key, string value) { + auto _s = db.prepare( + "INSERT OR REPLACE INTO metadata (doc_id, key, value)" + ~ " VALUES (NULL, :key, :value)" + ); + _s.bind(":key", key); + _s.bind(":value", value); + _s.execute(); + _s.finalize(); + } void insertBlock(string prefix, string[] keys_in_order, string[string] kv) { foreach (k; keys_in_order) { if (k in kv) { insertMeta(prefix ~ k, kv[k]); } @@ -295,8 +358,8 @@ template spineAbstractionDb() { version is the .ssp's own: the two serialisations are one format and move together +/ - insertMeta("schema.name", "sisu-abstraction-db"); - insertMeta("schema.version", ssp_format_version); + insertFileMeta("schema.name", "sisu-abstraction-db"); + insertFileMeta("schema.version", ssp_format_version); insertMeta("source.filename", ssp_doc.source); insertBlock("source.", ssp_doc.source_info_order, ssp_doc.source_info); /+ ↓ the digest of the .ssp this database was built from. a file cannot @@ -315,7 +378,7 @@ template spineAbstractionDb() { /+ ↓ populate objects +/ auto obj_stmt = db.prepare( "INSERT INTO objects (" - ~ "section, seq, ocn, is_a, is_of_part, is_of_section, is_of_type," + ~ "doc_id, section, seq, ocn, is_a, is_of_part, is_of_section, is_of_type," ~ "heading_level, heading_lev_collapsed, identifier," ~ "parent_ocn, parent_lev, last_descendant_ocn," ~ "children, ancestors, ancestors_collapsed," @@ -330,7 +393,7 @@ template spineAbstractionDb() { ~ "table_cols, table_widths, table_aligns, table_header," ~ "code_linenumbers, text" ~ ") VALUES (" - ~ ":section, :seq, :ocn, :is_a, :is_of_part, :is_of_section, :is_of_type," + ~ ":doc_id, :section, :seq, :ocn, :is_a, :is_of_part, :is_of_section, :is_of_type," ~ ":heading_level, :heading_lev_collapsed, :identifier," ~ ":parent_ocn, :parent_lev, :last_descendant_ocn," ~ ":children, :ancestors, :ancestors_collapsed," @@ -375,6 +438,7 @@ template spineAbstractionDb() { if (section_objs.length == 0) continue; foreach (seq, obj; section_objs) { + obj_stmt.bind(":doc_id", _doc_id); obj_stmt.bind(":section", section); obj_stmt.bind(":seq", cast(int) seq); obj_stmt.bind(":ocn", obj.metainfo.ocn); @@ -648,8 +712,23 @@ template spineAbstractionDb() { } /+ ↓ build the full text index from the rows just written +/ - db.run("INSERT INTO objects_fts(rowid, text)" - ~ " SELECT id, text FROM objects WHERE text IS NOT NULL"); + /+ ↓ scoped to this document, not the whole table. + (objects_fts is external content over objects.id, so re-running this + for a second language would re-insert every row the first language + put in: duplicate rowids, which fts5 does not refuse and which give + duplicated and wrong results. Nothing in the test suite queries the + index, so this one would not have been caught.) + +/ + { + auto _fts = db.prepare( + "INSERT INTO objects_fts(rowid, text)" + ~ " SELECT id, text FROM objects" + ~ " WHERE doc_id = :doc_id AND text IS NOT NULL" + ); + _fts.bind(":doc_id", _doc_id); + _fts.execute(); + _fts.finalize(); + } db.run("COMMIT TRANSACTION"); } |
