IVSoftware.Portable.SQLiteMarkdown 1.0.0

Prefix Reserved
dotnet add package IVSoftware.Portable.SQLiteMarkdown --version 1.0.0
                    
NuGet\Install-Package IVSoftware.Portable.SQLiteMarkdown -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="IVSoftware.Portable.SQLiteMarkdown" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="IVSoftware.Portable.SQLiteMarkdown" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="IVSoftware.Portable.SQLiteMarkdown" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add IVSoftware.Portable.SQLiteMarkdown --version 1.0.0
                    
#r "nuget: IVSoftware.Portable.SQLiteMarkdown, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package IVSoftware.Portable.SQLiteMarkdown@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=IVSoftware.Portable.SQLiteMarkdown&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=IVSoftware.Portable.SQLiteMarkdown&version=1.0.0
                    
Install as a Cake Tool

Expression Parsing

This cross-platform library supports expression-based filtering and search for SQLite-backed collections. Input text is parsed into SQL using structured rules for substrings and tags with atomicity for "quoted phrases".

Designed for a common shape

This isn’t a full SQL parser or a precision query engine — and it doesn’t try to be. It won’t replace hand-crafted queries when those are needed.

Instead, it’s a lightweight utility that captures the gestalt of a dataset in a useful, pragmatic way — tuned for the probable and almost-certain UI shape:

  • A platform-specific list view (WinForms, MAUI, WPF, etc.)
  • Driven by a shared navigation search bar
  • Where one input field controls both what’s shown and how it’s refined

It works well in situations where you want to support both browsing and filtering without re-engineering your data layer or your UI, and all it asks in return is that you decorate your target properties using the small set of [MarkdownTerm] attributes available in this package.

Mode Description
FILTER Filter an in-memory list (e.g., app settings, enums, cached results)
QUERY Query a remote source (e.g., cloud database, REST API)
QUERY → FILTER Query a remote source, then refine results using SQLite-backed filtering

Demo App included in this Repo

This library is platform-agnostic. At the same time, it drops any pretense of not knowing there's a UI out there with a "probable almost-certain shape" to it. That predicted UI is unapologetically supported with states and bindable properties.

Search demo with and without [app] tag

Search demo with [app] tag

The included demo is WinForms but don't be misled by that. There are no platform dependencies in the core package. None.


Expression Syntax Documentation

This README outlines the operators and rules for parsing expressions in a custom search language. The operators follow a standard order of operations and offer flexible syntax for various logical expressions.

Table of Contents

  1. Operators
  2. Escaped Operators
  3. Logical Operators
  4. Atomic Term Operators

Operators

Operators adhere to the standard order of operations, with PARENTHESES providing the functionality to override the default precedence.

Escaped Operators

The following characters can be escaped to be treated as literals in expressions:

  • \&, \|, \!, \(, \), \', \", "", ''

Logical Operators

AND Operator

The AND operation can be represented in several ways:

  • A single & character.
  • A single space .
  • Any consecutive string of & and space characters.

Examples:

  • "Term1 Term2" is interpreted as "Term1&Term2".
  • "Term1 & Term2" is interpreted as "Term1&Term2".

OR Operator

The OR operation can be represented as:

  • A single | character.
  • Any consecutive string of | characters and spaces.

Examples:

  • "Term1 | Term2" is interpreted as "Term1|Term2".
  • "Term1||Term2" is interpreted as "Term1|Term2".

NOT Operator

The NOT operation can be represented as:

  • A single ! character.
  • Any consecutive string of ! characters.

Examples:

  • "Term1 !Term2" is interpreted as "Term1&!Term2".
  • "Term1 !(Term2 | Term3)" is interpreted as "Term1&!(Term2|Term3)".

Linting Requirements

Expressions must be formatted to avoid consecutive identical operators or conflicting operators, such as &|.


Atomic Term Operators

TAG

Expressions that include tokens enclosed in square brackets ([ and ]) are treated as tags.

Special Rule

While a user is entering a search term, the parser is invoked any time the input delay settles. During this process:

  • Unmatched square brackets (either an opening [ without a closing ], or vice versa) are considered to be literal characters and are included in LIKE and FILTER expressions without interpretation as tags.

Single Quotes

Single quotes (') are used to define atomic (exact match) terms.

  • Quotes must appear in pairs to be interpreted as atomic delimiters.
  • If an expression contains a single quote, it is treated as a literal and included in the search term.
  • Consecutive single quotes ('') are interpreted as an escaped quote.
  • During incremental input (e.g. debounced typing), a trailing unpaired quote is always treated as a literal to preserve user intent mid-expression.

Double Quotes

Double quotes (") follow the same rules as single quotes.

  • Must appear in pairs to define atomic terms.
  • A lone double quote is treated as a literal character.
  • "" is interpreted as an escaped double quote.
  • During incremental input, a trailing unpaired quote is always treated as literal.

By following these rules, expressions can be parsed flexibly and safely—even while a user is still typing.


Expression to SQL Translation

Once your model is decorated with markdown attributes like [QueryLikeTerm], parsed expressions are converted into SQL using the attribute definitions. Each term is matched across all decorated fields, joined with OR.

For example:

public class PetProfile
{
    [QueryLikeTerm]
    public string Name { get; set; }

    [QueryLikeTerm]
    public string Species { get; set; }
}

Input like "pet" will match:

(Name LIKE '%pet%' OR Species LIKE '%pet%')

Here are more examples:

Input Expression SQL Translation (simplified) Description
cat dog (Name LIKE '%cat%' OR Species LIKE '%cat%') AND (Name LIKE '%dog%' OR Species LIKE '%dog%') Implicit AND
cat & dog (Name LIKE '%cat%' OR Species LIKE '%cat%') AND (Name LIKE '%dog%' OR Species LIKE '%dog%') Explicit AND
cat &&& dog (Name LIKE '%cat%' OR Species LIKE '%cat%') AND (Name LIKE '%dog%' OR Species LIKE '%dog%') Redundant AND syntax normalized
cat | dog (Name LIKE '%cat%' OR Species LIKE '%cat%') OR (Name LIKE '%dog%' OR Species LIKE '%dog%') OR operator
cat || dog (Name LIKE '%cat%' OR Species LIKE '%cat%') OR (Name LIKE '%dog%' OR Species LIKE '%dog%') Redundant OR syntax normalized
cat !dog (Name LIKE '%cat%' OR Species LIKE '%cat%') AND NOT (Name LIKE '%dog%' OR Species LIKE '%dog%') AND with NOT
!cat NOT (Name LIKE '%cat%' OR Species LIKE '%cat%') Single NOT
\!cat (Name LIKE '%!cat%' OR Species LIKE '%!cat%') Escaped NOT — treated as literal
!(cat | dog) NOT ((Name LIKE '%cat%' OR Species LIKE '%cat%') OR (Name LIKE '%dog%' OR Species LIKE '%dog%')) Negated group
'exact phrase' (Name LIKE '%exact phrase%' OR Species LIKE '%exact phrase%') Exact match using single quotes
"exact phrase" (Name LIKE '%exact phrase%' OR Species LIKE '%exact phrase%') Exact match using double quotes
\"Hello\" (Name LIKE '%""Hello""%' OR Species LIKE '%""Hello""%') Literal quotes via escaping

Matching is case-insensitive.


Split Contracts – Query Templates for Expression Parsing

So let’s be clear. We’ve used a class to generate a SQL expression. When we perform the actual query, does the data type receiving the recordset need to be the same type? It does not!

That’s the idea behind Split Contracts — you can separate the type used to build the query from the type used to receive the results. The query model is just a template. It defines how to interpret the input expression, not how the data is stored or shaped.

This lets you create purpose-specific templates that filter the same table in different ways. Want to search just by Name? Or only Species? Or maybe apply a strict tag match? Define a few small query classes and switch between them on the fly — even bind them to a dropdown in the UI.

class SearchByName     { [QueryLikeTerm] public string Name { get; set; } }
class SearchBySpecies  { [QueryLikeTerm] public string Species { get; set; } }
class SearchByTag      { [TagMatchTerm]  public string Tags { get; set; } }

Each of these can use the same search input — but produce different SQL depending on the fields and attributes involved.

Think of Split Contracts as little search adapters: they don't hold the data, they shape the search.

This pattern lets you:

  • Apply different query behaviors for different views, roles, or modes
  • Avoid annotating your core data models with filter-specific concerns
  • Cleanly separate indexing logic from data logic

Query templates are lightweight and composable — think of them as named filter contracts for how a user’s input should be interpreted.


SelfIndexing Class

The SelfIndexing class enables automatic generation of SQL search terms from property values using simple attribute annotations. It tracks changes in data, throttles processing intelligently, and maintains up-to-date searchable properties (QueryTerm, FilterTerm, TagMatchTerm) for fast, expression-based querying over markdown-bound SQLite objects.

One easy way to take advantage of this scheme is to inherit from the SelfIndexed base class, apply [PrimaryKey] to your ID property, and annotate other properties with e.g. [SelfIndexed(IndexingMode.QueryOrFilter)] to control how they contribute to indexing and persistence.

Note:
These indexing attributes — [QueryLikeTerm], [FilterLikeTerm], and [TagMatchTerm] — typically map directly to individual SQL clauses.
However, when used via [SelfIndexed] in a class derived from SelfIndexed, those values are aggregated into unified properties like QueryTerm, FilterTerm, and TagMatchTerm.
This makes SelfIndexing especially well-suited for filtering and full-text search scenarios, where a consolidated expression better reflects user intent.

SelfIndexing


ObservableQueryFilterSource

Drop-in replacement for ObservableCollection<T> with built-in support for both Query and Query-then-Filter workflows. It exposes a declarative interface for managing collection state while tracking query/filter intent via an internal FSM (QueryFilterStateTracker). Though UI-agnostic, the class anticipates integration with a navigation search bar, where queries are externally applied and subsequent in-memory filtering is handled via an embedded SQLite store. This enables persistent introspection of the original query, filtered/unfiltered results, and search metadata—all without any UI dependencies.

ObservableQueryFilterSource

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 95 7/12/2025
1.0.0-rc1 59 7/4/2025
1.0.0-beta1 135 6/22/2025
1.0.0-beta 113 6/21/2025

Initial 1.0.0 stable release.
- Finalized public interfaces and method contracts. No breaking changes expected post-1.0.
- Deprecated APIs (marked [Obsolete] or suffixed with OR) are excluded from future guarantees.
- Fully tested against standard workflows, including query-then-filter.
- Query-only and filter-only modes are functional but flagged for further test expansion.
- See the repo for a functional demo (WinForms) you can run out of the box.