Commit a4f680b4 authored by Charles Lindsay's avatar Charles Lindsay

Basic search table implementation; fix #6766

This is a limited implementation, so please backup your database before
running this search feature branch from now on as we may change things.

It's using a Unicode Snowball stemming tokenizer available from
https://github.com/littlesavage/sqlite3-unicodesn, also handily
available in src/sqlite3-unicodesn in Geary.  If you want to look at the
search tables on the command line, cd into the unicodesn source folder,
run make and make install, then load sqlite3 like:

   sqlite3 -cmd '.load unicodesn.sqlext' /path/to/geary.db
parent f5ba36ce
Copyright (c) 2001, Dr Martin Porter, and (for the Java developments) Copyright
(c) 2002, Richard Boulton
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
......@@ -13,3 +13,27 @@ License: LGPL-2.1
On Debian systems, the complete text of the GNU Lesser General Public
License 2.1, can be found in /usr/share/common-licenses/LGPL-2.1.
Files: src/sqlite3-unicodesn/libstemmer_c/*
Copyright: 2001, Dr Martin Porter
2002, Richard Boulton
License: BSD-2-clause
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
.
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
.
Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
......@@ -9,3 +9,4 @@ install(FILES version-006.sql DESTINATION ${SQL_DEST})
install(FILES version-007.sql DESTINATION ${SQL_DEST})
install(FILES version-008.sql DESTINATION ${SQL_DEST})
install(FILES version-009.sql DESTINATION ${SQL_DEST})
install(FILES version-010.sql DESTINATION ${SQL_DEST})
--
-- Dummy database upgrade to add MessageSearchTable, whose parameters depend on
-- things we need at run-time. See src/engine/imap-db/imap-db-database.vala in
-- post_upgrade() for the code that runs the upgrade.
--
......@@ -442,7 +442,7 @@ OPTIONS
)
add_library(geary-static STATIC ${ENGINE_VALA_C})
target_link_libraries(geary-static ${DEPS_LIBRARIES} gthread-2.0)
target_link_libraries(geary-static ${DEPS_LIBRARIES} sqlite3-unicodesn gthread-2.0)
# Geary client app
#################################################
......@@ -580,3 +580,4 @@ set_property(
gearyd
)
add_subdirectory(sqlite3-unicodesn)
......@@ -256,6 +256,15 @@ public class Geary.Email : BaseObject {
this.attachments.add_all(attachments);
}
public string get_searchable_attachment_list() {
StringBuilder search = new StringBuilder();
foreach (Geary.Attachment attachment in attachments) {
search.append(attachment.filename);
search.append("\n");
}
return search.str;
}
/**
* This method requires Geary.Email.Field.HEADER and Geary.Email.Field.BODY be present.
* If not, EngineError.INCOMPLETE_MESSAGE is thrown.
......
......@@ -18,6 +18,18 @@ public abstract class Geary.MessageData.AbstractMessageData : BaseObject {
public abstract string to_string();
}
/**
* Allows message data fields to define how they'll expose themselves to search
* queries.
*/
public interface Geary.MessageData.SearchableMessageData {
/**
* Return a string representing the data as a corpus of text to be searched
* against. Return values from this may be stored in the search index.
*/
public abstract string to_searchable_string();
}
public abstract class Geary.MessageData.StringMessageData : AbstractMessageData,
Gee.Hashable<StringMessageData> {
public string value { get; private set; }
......
......@@ -4,6 +4,8 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
extern int sqlite3_unicodesn_register_tokenizer(Sqlite.Database db);
private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
private const string DB_FILENAME = "geary.db";
private string account_owner_email;
......@@ -32,6 +34,10 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
case 6:
post_upgrade_encode_folder_names();
break;
case 10:
post_upgrade_add_search_table();
break;
}
}
......@@ -77,11 +83,76 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
}
}
// Version 10.
private void post_upgrade_add_search_table() {
try {
// This can't go in the .sql file because its schema (the stemmer
// algorithm) is determined at runtime.
string stemmer = "english"; // TODO
exec("""
CREATE VIRTUAL TABLE MessageSearchTable USING fts4(
id INTEGER PRIMARY KEY,
body,
attachment,
subject,
from_field,
receivers,
cc,
bcc,
tokenize=unicodesn "stemmer=%s",
prefix="2,4,6,8,10",
);
""".printf(stemmer));
} catch (Error e) {
error("Error creating search table: %s", e.message);
}
bool done = false;
int limit = 100;
for (int offset = 0; !done; offset += limit) {
try {
exec_transaction(Db.TransactionType.RW, (cx) => {
Db.Statement stmt = prepare(
"SELECT id FROM MessageTable ORDER BY id LIMIT ? OFFSET ?");
stmt.bind_int(0, limit);
stmt.bind_int(1, offset);
Db.Result result = stmt.exec();
if (result.finished)
done = true;
while (!result.finished) {
int64 id = result.rowid_at(0);
try {
MessageRow row = Geary.ImapDB.Folder.do_fetch_message_row(
cx, id, Geary.ImapDB.Folder.REQUIRED_FOR_SEARCH, null);
Geary.Email email = row.to_email(-1, new Geary.ImapDB.EmailIdentifier(id));
Geary.ImapDB.Folder.do_add_attachments(cx, email, id);
Geary.ImapDB.Folder.do_add_email_to_search_table(cx, id, email, null);
} catch (Error e) {
debug("Error adding message %lld to the search table: %s", id, e.message);
}
result.next();
}
return Db.TransactionOutcome.DONE;
});
} catch (Error e) {
debug("Error populating search table: %s", e.message);
}
}
}
private void on_prepare_database_connection(Db.Connection cx) throws Error {
cx.set_busy_timeout_msec(Db.Connection.RECOMMENDED_BUSY_TIMEOUT_MSEC);
cx.set_foreign_keys(true);
cx.set_recursive_triggers(true);
cx.set_synchronous(Db.SynchronousMode.OFF);
sqlite3_unicodesn_register_tokenizer(cx.db);
}
}
......@@ -6,6 +6,10 @@
private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
public const Geary.Email.Field REQUIRED_FOR_DUPLICATE_DETECTION = Geary.Email.Field.PROPERTIES;
public const Geary.Email.Field REQUIRED_FOR_SEARCH =
Geary.Email.Field.ORIGINATORS | Geary.Email.Field.RECEIVERS |
Geary.Email.Field.SUBJECT | Geary.Email.Field.HEADER | Geary.Email.Field.BODY |
Geary.Attachment.REQUIRED_FIELDS;
private const int LIST_EMAIL_CHUNK_COUNT = 5;
private const int LIST_EMAIL_FIELDS_CHUNK_COUNT = 500;
......@@ -909,6 +913,8 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
if (email.fields.fulfills(Attachment.REQUIRED_FIELDS))
do_save_attachments(cx, message_id, email.get_message().get_attachments(), cancellable);
do_add_email_to_search_table(cx, message_id, email, cancellable);
MessageAddresses message_addresses =
new MessageAddresses.from_email(account_owner_email, email);
foreach (Contact contact in message_addresses.contacts)
......@@ -918,6 +924,38 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
return true;
}
internal static void do_add_email_to_search_table(Db.Connection cx, int64 message_id,
Geary.Email email, Cancellable? cancellable) throws Error {
string? body = null;
try {
body = email.get_message().get_searchable_body();
} catch (Error e) {
// Ignore.
}
string? recipients = null;
try {
recipients = email.get_message().get_searchable_recipients();
} catch (Error e) {
// Ignore.
}
Db.Statement stmt = cx.prepare("""
INSERT INTO MessageSearchTable
(id, body, attachment, subject, from_field, receivers, cc, bcc)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""");
stmt.bind_rowid(0, message_id);
stmt.bind_string(1, body);
stmt.bind_string(2, email.get_searchable_attachment_list());
stmt.bind_string(3, (email.subject != null ? email.subject.to_searchable_string() : null));
stmt.bind_string(4, (email.from != null ? email.from.to_searchable_string() : null));
stmt.bind_string(5, recipients);
stmt.bind_string(6, (email.cc != null ? email.cc.to_searchable_string() : null));
stmt.bind_string(7, (email.bcc != null ? email.bcc.to_searchable_string() : null));
stmt.exec_insert();
}
private Gee.List<Geary.Email>? do_list_email(Db.Connection cx, Gee.List<LocationIdentifier> locations,
Geary.Email.Field required_fields, ListFlags flags, Cancellable? cancellable) throws Error {
Gee.List<Geary.Email> emails = new Gee.ArrayList<Geary.Email>();
......@@ -1242,6 +1280,38 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
updated_contacts = message_addresses.contacts;
}
private void do_merge_email_in_search_table(Db.Connection cx, int64 message_id,
Geary.Email email, Cancellable? cancellable) throws Error {
string? body = null;
try {
body = email.get_message().get_searchable_body();
} catch (Error e) {
// Ignore.
}
string? recipients = null;
try {
recipients = email.get_message().get_searchable_recipients();
} catch (Error e) {
// Ignore.
}
Db.Statement stmt = cx.prepare("""
UPDATE MessageSearchTable
SET body=?, attachment=?, subject=?, from_field=?, receivers=?, cc=?, bcc=?
WHERE id=?
""");
stmt.bind_string(0, body);
stmt.bind_string(1, email.get_searchable_attachment_list());
stmt.bind_string(2, (email.subject != null ? email.subject.to_searchable_string() : null));
stmt.bind_string(3, (email.from != null ? email.from.to_searchable_string() : null));
stmt.bind_string(4, recipients);
stmt.bind_string(5, (email.cc != null ? email.cc.to_searchable_string() : null));
stmt.bind_string(6, (email.bcc != null ? email.bcc.to_searchable_string() : null));
stmt.bind_rowid(7, message_id);
stmt.exec();
}
private void do_merge_email(Db.Connection cx, int64 message_id, Geary.Email email,
out Gee.Collection<Contact> updated_contacts, Cancellable? cancellable) throws Error {
assert(message_id != Db.INVALID_ROWID);
......@@ -1253,13 +1323,14 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
return;
// fetch message from database and merge in this email
MessageRow row = do_fetch_message_row(cx, message_id, email.fields | Attachment.REQUIRED_FIELDS,
cancellable);
MessageRow row = do_fetch_message_row(cx, message_id,
email.fields | REQUIRED_FOR_SEARCH | Attachment.REQUIRED_FIELDS, cancellable);
Geary.Email.Field db_fields = row.fields;
row.merge_from_remote(email);
// Build the combined email from the merge, which will be used to save the attachments
Geary.Email combined_email = row.to_email(email.position, email.id);
do_add_attachments(cx, combined_email, message_id, cancellable);
// Merge in any fields in the submitted email that aren't already in the database or are mutable
if (((db_fields & email.fields) != email.fields) || email.fields.is_any_set(Geary.Email.MUTABLE_FIELDS)) {
......@@ -1272,6 +1343,8 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
cancellable);
}
}
do_merge_email_in_search_table(cx, message_id, combined_email, cancellable);
}
private static Gee.List<Geary.Attachment>? do_list_attachments(Db.Connection cx, int64 message_id,
......
......@@ -4,7 +4,9 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class Geary.RFC822.MailboxAddress : BaseObject {
public class Geary.RFC822.MailboxAddress : Geary.MessageData.SearchableMessageData, BaseObject {
internal delegate string ListToStringDelegate(MailboxAddress address);
public string? name { get; private set; }
public string? source_route { get; private set; }
public string mailbox { get; private set; }
......@@ -124,8 +126,37 @@ public class Geary.RFC822.MailboxAddress : BaseObject {
: "%s <%s>".printf(GMime.utils_quote_string(name), address);
}
/**
* See Geary.MessageData.SearchableMessageData.
*/
public string to_searchable_string() {
return get_full_address();
}
public string to_string() {
return get_full_address();
}
internal static string list_to_string(Gee.List<MailboxAddress> addrs,
string empty, ListToStringDelegate to_s) {
switch (addrs.size) {
case 0:
return empty;
case 1:
return to_s(addrs[0]);
default:
StringBuilder builder = new StringBuilder();
foreach (MailboxAddress addr in addrs) {
if (!String.is_empty(builder.str))
builder.append(", ");
builder.append(to_s(addr));
}
return builder.str;
}
}
}
......@@ -4,7 +4,9 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class Geary.RFC822.MailboxAddresses : Geary.MessageData.AbstractMessageData, Geary.RFC822.MessageData {
public class Geary.RFC822.MailboxAddresses : Geary.MessageData.AbstractMessageData,
Geary.MessageData.SearchableMessageData, Geary.RFC822.MessageData {
public int size { get { return addrs.size; } }
private Gee.List<MailboxAddress> addrs = new Gee.ArrayList<MailboxAddress>();
......@@ -72,46 +74,20 @@ public class Geary.RFC822.MailboxAddresses : Geary.MessageData.AbstractMessageDa
return false;
}
public string to_rfc822_string() {
switch (addrs.size) {
case 0:
return "";
case 1:
return addrs[0].to_rfc822_string();
default:
StringBuilder builder = new StringBuilder();
foreach (MailboxAddress addr in addrs) {
if (!String.is_empty(builder.str))
builder.append(", ");
builder.append(addr.to_rfc822_string());
}
return builder.str;
}
return MailboxAddress.list_to_string(addrs, "", (a) => a.to_rfc822_string());
}
/**
* See Geary.MessageData.SearchableMessageData.
*/
public string to_searchable_string() {
return MailboxAddress.list_to_string(addrs, "", (a) => a.to_searchable_string());
}
public override string to_string() {
switch (addrs.size) {
case 0:
return "(no addresses)";
case 1:
return addrs[0].to_string();
default:
StringBuilder builder = new StringBuilder();
foreach (MailboxAddress addr in addrs) {
if (!String.is_empty(builder.str))
builder.append(", ");
builder.append(addr.to_string());
}
return builder.str;
}
return MailboxAddress.list_to_string(addrs, "(no addresses)", (a) => a.to_string());
}
}
......@@ -134,7 +134,8 @@ public class Geary.RFC822.Size : Geary.MessageData.LongMessageData, Geary.RFC822
}
}
public class Geary.RFC822.Subject : Geary.MessageData.StringMessageData, Geary.RFC822.MessageData {
public class Geary.RFC822.Subject : Geary.MessageData.StringMessageData,
Geary.MessageData.SearchableMessageData, Geary.RFC822.MessageData {
public const string REPLY_PREFACE = "Re:";
public const string FORWARD_PREFACE = "Fwd:";
......@@ -167,6 +168,13 @@ public class Geary.RFC822.Subject : Geary.MessageData.StringMessageData, Geary.R
return is_forward() ? new Subject(value) : new Subject("%s %s".printf(FORWARD_PREFACE,
value));
}
/**
* See Geary.MessageData.SearchableMessageData.
*/
public string to_searchable_string() {
return value;
}
}
public class Geary.RFC822.Header : Geary.MessageData.BlockMessageData, Geary.RFC822.MessageData {
......
......@@ -450,7 +450,53 @@ public class Geary.RFC822.Message : BaseObject {
return html_format ? get_text_body() : get_html_body();
}
}
/**
* Return the body as a searchable string. The body in this case should
* include everything visible in the message's body in the client, which
* would be only one body part, plus any visible attachments. Note that
* values that come out of this function are persisted.
*/
public string? get_searchable_body() {
string? body = null;
bool html = false;
try {
body = get_html_body();
html = true;
} catch (Error e) {
try {
body = get_text_body();
} catch (Error e) {
// Ignore.
}
}
if (body == null)
return null;
// TODO: add bodies of attached emails.
if (html) {
// FIXME: this is inadequate. For example, <br> needs to be turned
// into at least one space character, not just omitted. Also, we
// should also replace entities with the characters they represent.
body = Geary.HTML.remove_html_tags(body);
}
return body;
}
/**
* Return the full list of recipients (to, cc, and bcc) as a searchable
* string. Note that values that come out of this function are persisted.
*/
public string? get_searchable_recipients() {
Gee.List<RFC822.MailboxAddress>? recipients = get_recipients();
if (recipients == null)
return null;
return RFC822.MailboxAddress.list_to_string(recipients, "", (a) => a.to_searchable_string());
}
public Geary.Memory.AbstractBuffer get_content_by_mime_id(string mime_id) throws RFC822Error {
GMime.Part? part = find_mime_part_by_mime_id(message.get_mime_part(), mime_id);
if (part == null) {
......
# CMake definitions to build sqlite3-unicodesn as a static library. This file
# was added for Geary based on the project's Makefile.
set(STEMMERS
danish dutch english finnish french german hungarian
italian norwegian porter portuguese romanian russian
spanish swedish
)
set(SQLITE3_UNICODESN_SRC
fts3_unicode2.c
fts3_unicodesn.c
static.c
libstemmer_c/runtime/api_sq3.c
libstemmer_c/runtime/utilities_sq3.c
)
add_definitions(
-DSQLITE_ENABLE_FTS4
-DSQLITE_ENABLE_FTS4_UNICODE61
)
include_directories(
libstemmer_c/runtime
libstemmer_c/src_c
)
foreach(stemmer ${STEMMERS})
list(APPEND SQLITE3_UNICODESN_SRC libstemmer_c/src_c/stem_UTF_8_${stemmer}.c)
add_definitions(-DWITH_STEMMER_${stemmer})
endforeach()
add_library(sqlite3-unicodesn STATIC ${SQLITE3_UNICODESN_SRC})
target_link_libraries(sqlite3-unicodesn sqlite3)
CC?=gcc
#CFLAGS=-W -Wall -g -O0
CFLAGS?= -Os -DNDEBUG -s
DESTDIR?= /usr
STEMMERS?= danish dutch english finnish french german hungarian \
italian norwegian porter portuguese romanian russian \
spanish swedish
CFLAGS+= \
-DSQLITE_ENABLE_FTS4 \
-DSQLITE_ENABLE_FTS4_UNICODE61
SOURCES= \
fts3_unicode2.c \
fts3_unicodesn.c \
extension.c
HEADERS= fts3_tokenizer.h
INCLUDES= \
-Ilibstemmer_c/runtime \
-Ilibstemmer_c/src_c
LIBRARIES= -lsqlite3
SNOWBALL_SOURCES= \
libstemmer_c/runtime/api_sq3.c \
libstemmer_c/runtime/utilities_sq3.c
SNOWBALL_HEADERS= \
libstemmer_c/include/libstemmer.h \
libstemmer_c/runtime/api.h \
libstemmer_c/runtime/header.h
SNOWBALL_SOURCES+= $(foreach s, $(STEMMERS), libstemmer_c/src_c/stem_UTF_8_$(s).c)
SNOWBALL_HEADERS+= $(foreach s, $(STEMMERS), libstemmer_c/src_c/stem_UTF_8_$(s).h)
SNOWBALL_FLAGS+= $(foreach s, $(STEMMERS), -DWITH_STEMMER_$(s))
all: unicodesn.sqlext
unicodesn.sqlext: $(HEADERS) $(SOURCES) $(SNOWBALL_HEADERS) $(SNOWBALL_SOURCES)
$(CC) $(CFLAGS) $(SNOWBALL_FLAGS) $(INCLUDES) -fPIC -shared -fvisibility=hidden -o $@ \
$(SOURCES) $(SNOWBALL_SOURCES) $(LIBRARIES)
clean:
rm -f *.o unicodesn.sqlext
install: unicodesn.sqlext
mkdir -p ${DESTDIR}/lib 2> /dev/null
install -D -o root -g root -m 644 unicodesn.sqlext ${DESTDIR}/lib
.PHONY: clean install
SQLite3-unicodesn
==============
SQLite "unicode" full-text-search tokenizer with Snowball stemming
Installation
============
$ git clone git://github.com/littlesavage/sqlite3-unicodesn.git
$ cd sqlite3-unicodesn
$ make
$ su
# make install
Usage
======
$ sqlite3
sqlite> .load unicodesn.sqlext
sqlite> CREATE VIRTUAL TABLE fts USING fts3(text, tokenize=unicodesn "stemmer=russian");
sqlite> INSERT INTO fts VALUES ("Пионэры! Идите в жопу!");
sqlite> SELECT * FROM fts WHERE text MATCH 'Жопа';
Пионэры! Идите в жопу!
License
=======
Snowball files and stemmers are covered by the BSD license.
SQLite is in the Public Domain.
SQLite3-unicodesn code is in the Public Domain.
/*
** 2012 November 11
**
** The author disclaims copyright to this source code. In place of
** a legal notice, here is a blessing:
**
** May you do good and not evil.
** May you find forgiveness for yourself and forgive others.
** May you share freely, never taking more than you give.
**
******************************************************************************
**
*/
#include <sqlite3.h>
#include <sqlite3ext.h>
#include "fts3_unicodesn.h"
SQLITE_EXTENSION_INIT1