powershellsqlite

SQLite querying using embedded C#


How can I query an SQLite database using embedded C# code within PowerShell?


Solution

  • Thank you for sharing your solution, which is a lightweight way of using SQLite from PowerShell without needing to install modules or libraries.
    Additionally, it also works on ARM64 machines, whereas NuGet packages such as Microsoft.Data.Sqlite and System.Data.SQLite currently don't.

    I've taken the liberty of adapting your code in the following ways:


    Definition of static helper class [SqliteHelper] via Add-Type and ad hoc-compiled C# code that makes P/Invoke calls to the native SQLite library; incurs a one-time compilation performance penalty per session:

    # Compile helper type [SqliteHelper], which is a thin layer on top of the C/C++
    # SQLite API.
    # Gratefully adapted from https://stackoverflow.com/a/76488520/45375
    # Determine the platform-appropriate name of the native SQLite library.
    # Note: Tested on Windows and macOS.
    $libName = ('sqlite3', 'winsqlite3.dll')[$env:OS -eq 'Windows_NT']
    # Compile the code.
    # NOTE:
    #   * Re -ReferencedAssemblies:
    #      * System.Data.Common, System.Collections are needed for PS Core, System.Xml is needed for WinPS.
    #   * For the sake of WinPS compatibility:
    #     * The code below uses (a) legacy properyt-definition syntax and (b) legacy dictionary initializer syntax.
    #     * The *16() variants of the SQLite functions are used so that .NET (UTF-16LE) strings can be used / converted via
    #       Marshal.PtrToStringUni().
    #       Behind the scenes, SQLite still translates to and from UTF-8, but not having to deal with that on the .NET
    #       side makes things easier, given that only .NET (Core) suports Marshal.PtrToStringUTF8() 
    Add-Type -ReferencedAssemblies System.Collections, System.Data, System.Data.Common, System.Xml -TypeDefinition @"
    using System;
    using System.Data;
    using System.Collections.Generic;
    using System.Runtime.InteropServices;
    
    public static class SqliteHelper {
      [DllImport("$libName", CharSet=CharSet.Unicode, EntryPoint="sqlite3_open16")]            private static extern int    open(string filename, out IntPtr db);
      [DllImport("$libName", EntryPoint="sqlite3_extended_result_codes")]                      private static extern int    result_codes(IntPtr db, int onOrOff);
      [DllImport("$libName", EntryPoint="sqlite3_close_v2")]                                   private static extern int    close(IntPtr db);
      [DllImport("$libName", CharSet=CharSet.Unicode, EntryPoint="sqlite3_prepare16")]         private static extern int    prepare(IntPtr db, string query, int len, out IntPtr stmt, IntPtr dummy);
      [DllImport("$libName", EntryPoint="sqlite3_step")]                                       private static extern int    step(IntPtr stmt);
      [DllImport("$libName", EntryPoint="sqlite3_column_count")]                               private static extern int    column_count( IntPtr stmt);
      [DllImport("$libName", EntryPoint="sqlite3_column_name16")]                              private static extern IntPtr column_name(  IntPtr stmt, int col);
      [DllImport("$libName", EntryPoint="sqlite3_column_type")]                                private static extern int    column_type(  IntPtr stmt, int col);
      [DllImport("$libName", EntryPoint="sqlite3_column_double")]                              private static extern Double column_double(IntPtr stmt, int col);
      [DllImport("$libName", EntryPoint="sqlite3_column_int")]                                 private static extern int    column_int(   IntPtr stmt, int col);
      [DllImport("$libName", EntryPoint="sqlite3_column_int64")]                               private static extern Int64  column_int64( IntPtr stmt, int col);
      [DllImport("$libName", EntryPoint="sqlite3_column_text16")]                              private static extern IntPtr column_text(  IntPtr stmt, int col);
      [DllImport("$libName", EntryPoint="sqlite3_column_blob")]                                private static extern IntPtr column_blob(  IntPtr stmt, int col);
      [DllImport("$libName", EntryPoint="sqlite3_column_bytes")]                               private static extern int    column_bytes( IntPtr stmt, int col);
      [DllImport("$libName", EntryPoint="sqlite3_finalize")]                                   private static extern int    finalize(IntPtr stmt);
    
      // Important result codes.
      private const int SQLITE_OK = 0; 
      private const int SQLITE_ROW = 100; // step() indicates that at least 1 more row exists.
      private const int SQLITE_DONE = 101; // step() indicates that therea are no (more) rows.
      // Data type IDs
      private const int SQLITE_INTEGER = 1;
      private const int SQLITE_FLOAT = 2;
      private const int SQLITE_TEXT = 3;
      private const int SQLITE_BLOB = 4;
      private const int SQLITE_NULL = 5;
    
      // A helper exception to report SQLite result codes that are errors.
      public class SqliteException : Exception {
        private int _nativeErrorCode; 
        public int NativeErrorCode { get { return _nativeErrorCode; } set { _nativeErrorCode = value; } }
        public SqliteException(int code) : this(String.Format("SQLite API call failed with result code {0}.", code), code) {}
        public SqliteException(string message, int code) : base(message) { NativeErrorCode = code; }
      }
    
      public static IntPtr Open(string filename) {
          IntPtr db;
          int result = open(filename, out db);
          if (result != SQLITE_OK) throw new SqliteException(result);
          result = result_codes(db, 1); // report extended result codes by default.
          if (result != SQLITE_OK) throw new SqliteException(result);
          return db;
      }
    
      public static void Close(IntPtr db) {
          int result = close(db);
          if (result != SQLITE_OK) throw new SqliteException(result);
      }
    
      public static DataTable Execute(IntPtr db, string query) {
          IntPtr stmt;
          DataTable dt = new DataTable();
    
          int result = prepare(db, query, -1, out stmt, IntPtr.Zero);
          if (result != SQLITE_OK) throw new SqliteException(result);
    
          int colCount = column_count(stmt);
    
          // Get the first row so that column namescan be determined.
          result = step(stmt);
          if (result == SQLITE_ROW) {
            // Add corresponding columns to the data-table object.
            // NOTE: Since any column value can be NULL, we cannot infer fixed data
            //       types for the columns and therefore *must* use typeof(object).
            for (int c = 0; c < colCount; c++) {
              dt.Columns.Add(Marshal.PtrToStringUni(column_name(stmt, c)), typeof(object));
            }
          } else if (result == SQLITE_DONE) { // Either a query without results or a non-query statement.
            result = finalize(stmt);
            if (result != 0) throw new SqliteException(result);
            // For simplicity, return an empty DataTable instance either way.
            // In a PowerShell pipeline, its .Rows collection is automatically enumerated,
            // meaning that *no* objects are sent through the pipeline.
            // If a non-query's output isn't captured (which shouldn't be necessary), PowerShell's automatic enumeration
            // in the pipeline ensures that *no* output stream pollution occurs.
            return dt;
          } else {
            throw new SqliteException(result);
          }
    
          // Fetch all rows and populate a DataTable instance with them.
          object[] rowData = new object[colCount];
          do {
            for (int i = 0; i < colCount; i++) {
              // Note: The column types must be determined for each and every row,
              //       given that NULL values may be present.
              switch (column_type(stmt, i)) {
                case SQLITE_INTEGER: // covers all integer types up to System.Int64
                  rowData[i] = column_int64(stmt, i);
                  break;
                case SQLITE_FLOAT:
                  rowData[i] = column_double(stmt, i);
                  break;
                case SQLITE_TEXT:
                  rowData[i] = Marshal.PtrToStringUni(column_text(stmt, i));
                  break;
                case SQLITE_BLOB:
                  IntPtr ptr = column_blob(stmt, i);
                  int len = column_bytes(stmt, i);
                  byte[] arr = new byte[len];
                  Marshal.Copy(ptr, arr, 0, len);
                  rowData[i] = arr;
                  break;
                case SQLITE_NULL: 
                  rowData[i] = DBNull.Value;
                  break;
                default:
                  throw new Exception(String.Format("DESIGN ERROR: Unexpected column-type ID: {0}", column_type(stmt, i)));
              }
            }
            dt.Rows.Add(rowData);
          } while (step(stmt) == SQLITE_ROW);
    
          result = finalize(stmt);
          if (result != SQLITE_OK) throw new SqliteException(result);
    
          return dt;
      }
    }
    "@
    

    Sample usage (assumes that you've run the Add-Type command above beforehand):

    # Abort on (uncaught) errors.
    $ErrorActionPreference = 'Stop'
    
    # Open an in-memory database (':memory:')
    # To open - or create on demand - a database *file*,
    # pass its *full path* of the database file.
    # To convert a *relative* path to a full one:
    #  * If the file exists, use (Convert-Path $relativePath)
    #  * Otherwise, use "$PWD/$relativePath"
    $db = [SqliteHelper]::Open(':memory:')
    
    # NOTE:
    # Submit ONE statement at a time with ::Execute() (";" terminator is optional).
    
    # Create a sample table.
    # NOTE: This fails, if the table already exists.
    # Non-ASCII characters are included to test if character-encoding issues arise.
    [SqliteHelper]::Execute($db, @'
      create table sample_table (Name string, Äge int)
    '@)
    
    # Insert rows into the sample table.
    # Non-ASCII characters are included to test if character-encoding issues arise.
    # A 64-bit integer is included to test if overflow issues arise.
    [SqliteHelper]::Execute($db, @'
      insert into sample_table (Name, Äge) values ('JDoë', 42), ('JRoe', 5856288576210)
    '@)
     
    # Submit a sample query.
    $query = @'
    SELECT * FROM sample_table;
    '@
    $dataTable = [SqliteHelper]::Execute($db, $query)
    
    # Display the results.
    $dataTable
    
    # Close the database.
    [SqliteHelper]::Close($db)