โŒ About FreshRSS

Normal view

There are new articles available, click to refresh the page.
Before yesterdayNews from the Ada programming language world

How to get access to a record field

Some general context for my problem first.

I need to bind some arguments of a prepared statement using GNATCOLL SQLite bindings. These bindings expect C character pointer as an input (beside other things). This requirements creates two problems on Ada's end:

Problem 1

The variable pointed to by the "bound" pointer must not perish until the prepared statement is finalized (otherwise it will store a pointer to garbage). But, for queries that operate on the same type of record it would be desirable to extract the part of binding of arguments (which are obtained from the record fields) into a separate procedure. If such procedure returns before the statement is finalized the variables (on the stack of such procedure) will be deleted, and pointers now point to garbage.

Problem 2

I only know of three instances in Ada of creating pointers: new-ing, taking a pointer to a function / procedure and taking a pointer to a variable. Since the bindings want a pointer, I don't know how to extract such a pointer from a record unless I "manually unpack" it into a bunch of local variables. Needless to say this is ugly, repetitive and very prone to copy-and-paste error. This also leads to the lifetime issues (since variables "unpacked" in such a way will be deleted before the actual value they are used to capture still exists.)

Example

type Tag is record
  Field : String := "value";
end record;

type Tag_Access is access all Tag;

procedure Bind_Tag (T : Tag_Access; Stmt : Gnade.Statement) is
  --  This variable will vanish before the statement is executed
  Field : aliased constant String := T.Field;
begin
    Gnade.Bind_Text (Stmt, Field'Address, Field'Length);
end Bind_Tag;

procedure Insert_Tag (T : Tag) is
  --  Necessary connection initialization and building of prepared statement
  Tc : Tag := T;  --  Creating a useless variable only to please the compiler
  Ta : Tag_Access := Tc'Access;
begin
  Bind_Tag (Ta, Stmt);
  --  Here, bindings are dead, so we are executing garbage
  Gnade.Step (Db, Stmt);
end Insert_Tag;

If I may enter a plea

I suspect this may be helped by using objects (i.e. new-ing something). I haven't researched this approach because my first experience with Ada (I'm still learning) was very negative when contracting objects. Deallocation combined with absence of convenient object lifetime management (eg. equivalent of C++ RAII) makes using objects a very daunting task. I would like to stay away from this functionality as much as possible.


Edit

I found a way out of this particular conundrum: turns out SQlite can be instructed to make copies when binding strings. This isn't ideal, but at least I can get the strings into the database.

This doesn't mean that the question is solved though. I'd still like to know a more general way of dealing with record fields. Something that in eg. C would be accomplished by taking a pointer to the struct and then adding the size of the fields preceding the field of interest and adding that the the pointer.

What is "extension aggregate" and why do I need it?

I'm trying to use GNAT's SQLite bindings. And I want some way to get errors from these bindings. Trying to read through the code, I found that I can set up the connection by providing a mystery object that is supposed to handle errors...

No matter how I try to shape and provide this object, the library either won't accept it, or I get runtime access check errors (another bizarre aspect of this program as the pointer should be always alive since it's never leaving the scope of the procedure that declared it... but maybe it works differently in Ada).

The error I'm getting looks like this:

type of aggregate has private ancestor "Error_Reporter"
must use extension aggregate

The original Error_Reporter is defined as follows:

type Error_Reporter is abstract tagged private;
...
private

   type Error_Reporter is abstract tagged null record;

Anyways, below is the outline of the problematic code:

The header file:

   -- type Error_Handler is new Gse.Error_Reporter with private;
   type Error_Handler is new Gse.Error_Reporter with record
      Message : Asu.Unbounded_String;
   end record;
   
   type Error_Handler_Access is access Error_Handler;

   procedure On_Error
     (Self       : in out Error_Handler;
      Connection : access Gse.Database_Connection_Record'Class;
      Message    : String);

-- more stuff ...
   
-- private
--    type Error_Handler is new Gse.Error_Reporter with record
--       Message : Asu.Unbounded_String;
--    end record;

the implementation:

...
      declare
         pragma Suppress (Accessibility_Check);
         -- Handler : constant Error_Handler_Access :=
         --   new Error_Handler'(Message => Asu.Null_Unbounded_String);
         Handler : constant access Error_Handler :=
           new Error_Handler'(Message => Asu.Null_Unbounded_String);
         -- Handler : aliased Error_Handler;
         Descr         : Gse.Database_Description    :=
           Gss.Setup (Db_File, Errors => Handler);
--         Gss.Setup (Db_File, Errors => Handler'Access);
...

Commented code shows various things I tried. (Also, pragma has no effect).

Ideally, I don't want to new anything, unless this is the requirement from GNAT's interface.

Even better: if I could entirely avoid using this object (it should be possible to salvage the actual Sqlite connection from this wrapper and get access to return code and the error message), but the code is not very easy to read and is trying to make the user do things that I don't want (ORM, multiple layers of bindings that only complicate things) all while hiding the essential useful functionality. I'd appreciate any guidance in this direction.


UPDATE: In the end, my problem was I couldn't figure out how to use Bind_XXX group of procedures from Gnade package. And this is what prompted me to use the Execute package, which ultimately led to the problem with error reporting. To solve this, I finally figured out how to use Bind_XXX (the one giving me the most problems was Bind_Text due to pointer translation between Ada and C). Once that worked, I rewrote the code to use just Gnade.

Ada, Java and Python database access

17 November 2018 at 14:02

The database also has a serious impact on such benchmark and I've measured the following three famous databases:

The purpose of the benchmark is to be able to have a simple comparison between these different databases and different programming languages. For this, a very simple database table is created with only two integer columns one of them being the primary key with auto increment. For example the SQLite table is created with the following SQL:

CREATE table test_simple (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  value INTEGER
)

The database table is filled with a simple INSERT statement which is also benchmarked. The goal is not to demonstrate and show the faster insert method, nor the faster query for a given database or language.

Benchmark

The SQL benchmarks are simple and they are implemented in the same way for each language so that we can get a rough comparison between languages for a given database. The SELECT query retrieves all the database table rows but it includes a LIMIT to restrict the number of rows returned. The query is executed with different values for the limit so that a simple graph can be drawn. For each database, the SQL query looks like:

SELECT * FROM test_simple LIMIT 10

The SQL statements are executed 10000 times for SELECT queries, 1000 times for INSERT and 100 times for DROP/CREATE statements.

Each SQL benchmark program generates an XML file that contains the results as well as resource statistics taken from the /proc/self/stat file. An Ada tool is provided to gather the results, prepare the data for plotting and produce an Excel file with the results.

Python code

def execute(self):
  self.sql = "SELECT * FROM test_simple LIMIT " + str(self.expect_count)
  repeat = self.repeat()
  db = self.connection()
  stmt = db.cursor()

  for i in range(0, repeat):
    stmt.execute(self.sql)
    row_count = 0
    for row in stmt:
      row_count = row_count + 1

    if row_count != self.expect_count:
      raise Exception('Invalid result count:' + str(row_count))

    stmt.close()
Java code
public void execute() throws SQLException {
  PreparedStatement stmt
 = mConnection.prepareStatement("SELECT * FROM test_simple LIMIT " + mExpectCount);

  for (int i = 0; i < mRepeat; i++) {
    if (stmt.execute()) {
      ResultSet rs = stmt.getResultSet();
      int count = 0;
      while (rs.next()) {
        count++;
      }
      rs.close();
      if (count != mExpectCount) {
        throw new SQLException("Invalid result count: " + count);
      }
    } else {
      throw new SQLException("No result");
    }
  }
  stmt.close();
}
Ada code
procedure Select_Table_N (Context : in out Context_Type) is
   DB    : constant ADO.Sessions.Master_Session := Context.Get_Session;
   Count : Natural;
   Stmt  : ADO.Statements.Query_Statement
        := DB.Create_Statement ("SELECT * FROM test_simple LIMIT " & Positive'Image (LIMIT));
begin
   for I in 1 .. Context.Repeat loop
      Stmt.Execute;
      Count := 0;
      while Stmt.Has_Elements loop
         Count := Count + 1;
         Stmt.Next;
      end loop;
      if Count /= LIMIT then
         raise Benchmark_Error with "Invalid result count:" & Natural'Image (Count);
      end if;
   end loop;
end Select_Table_N;

The benchmark were executed on an Intel i7-3770S CPU @3.10Ghz with 8-cores running Ubuntu 16.04 64-bits. The following database versions are used:

  • MariaDB 10.0.36
  • PostgreSQL 9.5.14

Resource usage comparison

The first point to note is the fact that both Python and Ada require only one thread to run the SQL benchmark. On its side, the Java VM and database drivers need 20 threads to run.

The second point is not surprising: Java needs 1000% more memory than Ada and Python uses 59% more memory than Ada. What is measured is the the VM RSS size which means this is really the memory that is physically mapped at a given time.

The SQLite database requires less resource than others. The result below don't take into account the resource used by the MariaDB and PostgreSQL servers. At that time, the MariaDB server was using 125Mb and the PostgreSQL server was using 31Mb.

sql-memory.png

Speed comparison

Looking at the CPU time used to run the benchmark, Ada appears as a clear winner. The Java PostgreSQL driver appears to be very slow at connecting and disconnecting to the database, and this is the main reason why it is slower than others.

sql-time.png

It is interesting to note however that both Java and Python provide very good performance results with SQLite database when the number of rows returned by the query is less than 100. With more than 500 rows, Ada becomes faster than others.

sql-sqlite.png

With a PostgreSQL database, Ada is always faster even with small result sets.

sql-postgresql.png

sql-mysql.png

Conclusion and references

SQLite as an embedded database is used on more than 1 billion of devices as it is included in all smartphones (Android, iOS). It provides very good performances for small databases.

With client-server model, MariaDB and PostgreSQL are suffering a little when compared to SQLite.

For bigger databases, Ada provides the best performance and furthermore it appears to be more predictable that other languages (ie, linear curves).

The Excel result file is available in: sql-benchmark-results.xls

Sources of the benchmarks are available in the following GitHub repository:

โŒ
โŒ