[1.0.0] - 2026-09-12
v1.0 is a large rewrite and not a drop-in upgrade from the v0.16.x line. The motivating goal was to stop assuming every data source is SQL-on-Ecto: Lotus now wraps every source behind a uniform
Lotus.Source.Adaptercontract, threads an opaque%Statement{}through the pipeline (SQL text for Ecto, JSON / DSL / AST for everything else), lets each adapter own its own variable substitution, visibility extraction, and AI context, and renames the public surface away from*_repo*language to reflect that sources are no longer just repos. Configuration keys, middleware and telemetry payload shapes, DB column names, cache tags, and a chunk of the public API all moved. There is no@deprecatedcompatibility layer — pre-v1 apps must port deliberately. See the Upgrading to v1.0 guide for the step-by-step migration.
Breaking Changes
Adapter contract
Pluggable adapter architecture —
Lotus.Source.Adapteris the new universal behaviour + struct wrapping every data source.Lotus.Sourceis a public facade (not a behaviour) withresolve!/2,list_sources/0,get_source!/1,default_source/0,source_type/1,supports_feature?/2,hierarchy_label/1,example_query/3,query_language/1,limit_query/3(%Statement{}in,%Statement{}out, optional with a passthrough default),supported_filter_operators/1,prepare_for_analysis/2,name_from_module!/1. SQL-specific callbacks moved toLotus.Source.Adapters.Ecto.Dialect. TheLotus.Sourcesmodule and allLotus.Sources.*dialect modules (Postgres,MySQL,SQLite3,Default) were deleted. Ecto-backed adapters useuse Lotus.Source.Adapters.Ecto, dialect: MyDialect; per-dialect adapters (Lotus.Source.Adapters.Postgres,MySQL,SQLite3) are built with the same macro. Registration uses optionalcan_handle?/1+wrap/2callbacks driven by a new:source_adaptersconfig list. Host applications implementing@behaviour Lotus.Sourcewith a custom module will fail to compile and must port (#193).%Lotus.Query.Statement{}is the pipeline carrier. Pipeline callbacks (apply_filters/3,apply_sorts/3,apply_pagination/3,transform_bound_query/3,transform_statement/2) take and return a%Statement{}with:adapter(module),:body(adapter-opaqueterm()),:params, and:meta. Non-SQL adapters carry native payloads (JSON maps, DSL ASTs) in:bodywithout serializing to strings. Constructor:Lotus.Query.Statement.new(body, params \\ []).Lotus.Runner.run_statement/3takes(%Adapter{}, %Statement{}, opts).Lotus.Preflight.authorize/4takes(%Adapter{}, %Statement{}, search_path, scope). The:sql/:paramstuple shape is gone from the pipeline.Pagination count queries moved into
statement.meta[:count_spec].apply_pagination/3returns a single%Statement{}whose:metacarries the optional count spec instead of the old third tuple element.Two pagination count strategies for
count: :exact. Adapters pick how they surface the pre-pagination total. Strategy A — inline count:execute_query/4returns the total via a new optional:total_countkey in its result map, for engines where the count comes back as a side-effect of the main query (Elasticsearch'strack_total_hits: true, MongoDB's$facet).apply_pagination/3does not set:count_specin this mode. Strategy B — separate count query:apply_pagination/3places acount_specinstatement.meta[:count_spec]and Lotus core runs it through the same adapter (standard for SQL). Precedence: if both channels are present, the inline count wins andcount_specis not run.Result.meta.total_modenow reflects what the caller requested (:exact|:none), not how the adapter chose to fulfil it — so an adapter that cannot produce a total still reports:exacthonestly withtotal_count: nil.execute_query/4typespec widened to includeoptional(:total_count) => non_neg_integer() | nil; existing adapters that omit the key are unaffected.Variable substitution is adapter-owned. Two universal callbacks —
substitute_variable/5andsubstitute_list_variable/5— let each adapter pick its substitution strategy. SQL-prepared adapters add a placeholder ($1,?, ...) tostatement.bodyand push the value intostatement.params. JSON / DSL adapters (Elasticsearch, Mongo) inline the value as a properly-escaped literal — they are the injection boundary and must escape through the language's native encoder. Adapters with no{{var}}mental model return{:error, :unsupported}.Lotus.Storage.Query.compile/2is now adapter-agnostic — it threads a%Statement{}through a reduce loop and delegates toAdapter.substitute_variable/5, never touching placeholders or param arrays directly. Removed:FilterInjector.quote_value/1(values are now parameterized). Removed from the universal behaviour:param_placeholder/4andlimit_offset_placeholders/3— these were SQL-prepared-statement primitives and now live onLotus.Source.Adapters.Ecto.Dialectas Ecto-internal.Visibility preflight returns
{:ok, MapSet} | {:error, reason} | {:unrestricted, reason}.extract_accessed_resources/2signals{:unrestricted, reason}when visibility cannot be enforced at the adapter layer (e.g. Elasticsearch's index-level access control).Lotus.Preflightgates these statements behind the new:allow_unrestricted_resourcesconfig (global + per-source). Opted-in sources pass; unopted sources get an actionable error instructing the operator how to opt in. Preflight no longer sniffs SQL prefixes — a newneeds_preflight?/2adapter callback controls the skip path (built-in Ecto adapter retains theEXPLAIN/SHOW/PRAGMAheuristic internally).Callback renames — introspection "schema" double meaning. The word "schema" meant two things (namespace vs. column definitions). Hard renames, no aliases:
get_table_schema/3→describe_table/3,resolve_table_schema/3→resolve_table_namespace/3,explain_plan/4→query_plan/3(now takes(state, %Statement{}, opts); return widened to{:ok, String.t() | nil} | {:error, term()}so non-SQL engines can return{:ok, nil}without surfacing an error). Renames apply toLotus.Source.Adapter,Lotus.Source.Adapters.Ecto.Dialect, all four built-in Ecto dialect impls, and the middle-layerLotus.Schema.get_table_schema/3.list_schemas/1andlist_tables/3are unchanged — "schemas" as namespaces is widely understood adapter terminology and doesn't carry the column double-meaning.Callback signatures take
stateas the first argument for SQL-generation (quote_identifier/2,query_plan/3) and error-handling (format_error/2) callbacks.execute_query/4typespec widened —sql :: String.t()→sql :: term(). This is the driver boundary; adapters receive the adapter-native statement payload. Dialyzer builds that pattern-matched the oldString.t()spec should relax.New universal callbacks for feature-driven non-SQL parity:
validate_statement/3(SQL: EXPLAIN; ES:_validate; default:oktrust-on-execute),parse_qualified_name/2(returns an ordered hierarchy list),validate_identifier/3(per-kind identifier grammar),supported_filter_operators/1(adapter declares which filter operators itsapply_filters/3handles — core raisesLotus.UnsupportedOperatorErroron mismatch, no silent degradation). Filter/sort column names are validated viavalidate_identifier/3before dispatch — unsafe identifiers raiseArgumentError.New
ai_context/1+prepare_for_analysis/2callbacks.ai_context/1returns{:ok, map}with:language(must match^[a-z0-9]+:[a-z0-9_-]+$),:example_query(≤ 2 KB),:syntax_notes(≤ 1 KB),:error_patterns(≤ 20 entries of%{pattern: Regex.t(), hint: binary}), and optional:capabilities(%{generation, optimization, explanation}— eachtrue | {false, reason}). Returning{:error, _}opts the source out of AI entirely. Size limits and language-regex enforced at the dispatch layer with one-timeLogger.warning/1per adapter.AI trust boundary. The new
:trusted_source_adaptersconfig allowlists adapter modules whoseai_context/1free-form fields (and capability reasons) flow unchanged into the LLM prompt. Built-inLotus.Source.Adapters.Ecto+ its per-dialect wrappers are always trusted. Untrusted adapters supply only:language; free-form fields are stripped and capability reasons are replaced with a generic fallback to bound prompt-injection blast radius.
Configuration
Renamed config keys — all hard renames, no alias. Validation fails outright on the old names (no
normalize_deprecated_keys/1)::ecto_repo→:storage_repo(accessorLotus.repo/0unchanged):data_repos→:data_sources(widened to{:map, :string, {:or, [:atom, :map]}}so non-Ecto adapters can pass config maps, e.g.%{adapter: :elasticsearch, url: "http://..."}):default_repo→:default_source
New config keys:
:source_adapters,:trusted_source_adapters,:allow_unrestricted_resources,:source_resolver(defaultLotus.Source.Resolvers.Static),:visibility_resolver(defaultLotus.Visibility.Resolvers.Static).Lotus.Config.get_data_source!/1return type widened frommodule()tomodule() | map()(anddata_sources/0likewise). Code that unconditionally pattern-matched the return as a module atom — e.g.repo = Config.get_data_source!(name); repo.query!(...)— may now receive a map. Dialyzer will flag these call sites; runtime behaviour is preserved for the Ecto path.
Public API
Renamed / removed functions (no aliases):
Lotus.run_sql/3→Lotus.run_statement/3Lotus.Runner.run_sql/4→Lotus.Runner.run_statement/3(now takes%Adapter{}+%Statement{};sql/paramsreplaced)Lotus.get_table_schema/3→Lotus.describe_table/3Lotus.Schema.get_table_schema/3→Lotus.Schema.describe_table/3- Removed:
Lotus.data_repos/0,get_data_repo!/1,list_data_repo_names/0,default_data_repo/0— useLotus.Source.list_sources/0,get_source!/1,Lotus.list_data_source_names/0,Lotus.Source.default_source/0. - Removed:
Lotus.Config.data_repos/0,get_data_repo!/1,list_data_repo_names/0,default_data_repo/0,rules_for_repo_name/1,schema_rules_for_repo_name/1,column_rules_for_repo_name/1— use the*_source*variants. - Removed:
Lotus.Sourcedeprecated dispatch functions (execute_in_transaction,set_statement_timeout,set_search_path,list_schemas,list_tables,explain_plan,quote_identifier,param_placeholder,limit_offset_placeholders,apply_filters,apply_sorts,format_error,builtin_denies,builtin_schema_denies,default_schemas). UseLotus.Source.Adapterdispatch helpers instead. - Removed:
Lotus.Storage.TypeMapper— type mapping now happens via dialectdb_type_to_lotus_type/1callbacks.
Lotus.Storage.Query.to_sql_params/2renamed tocompile/2and now returns{:ok, %Lotus.Query.Statement{}} | {:error, reason}instead of a bare{sql, params}tuple.compile!/2is the raising variant and returns the%Statement{}directly (#163).@type repoinLotus.Sourcerenamed to@type source_module.
Storage
DB column
data_repo→data_sourceinlotus_queries. New installs get the updated column name directly from the migration chain. Upgrading Postgres installs get a conditionalALTER TABLE ... RENAME COLUMNviaLotus.Migrations.Postgres.V4— runmix ecto.migrateafter upgrading. MySQL / SQLite users must run the rename manually (ALTER TABLE lotus_queries RENAME COLUMN data_repo TO data_source) before starting app code against the new schema.Lotus.Storage.Query.data_repofield renamed (Elixir side) and thesource: :data_reposhim removed.New
query_languagecolumn onlotus_queries(nullable, 32 characters). Records thefamily:dialectidentifier —sql:postgres,json:elasticsearch— that a saved query was written for, exposed asLotus.Storage.Query.query_language.NULLmeans "derive it from the source's adapter", which is how every pre-existing row behaves, so there is no backfill. Postgres installs get it fromLotus.Migrations.Postgres.V5viamix ecto.migrate; MySQL and SQLite users must add it manually (ALTER TABLE lotus_queries ADD COLUMN query_language VARCHAR(32)/... TEXT) alongside thedata_sourcerename above.Lotus.run_query/2refuses a language mismatch. When a saved query'squery_languagediffers from the resolved source's, execution returns{:error, msg}naming both languages and the source, instead of passing the statement to an engine that cannot parse it. The comparison is exact, not family-level:sql:postgresandsql:clickhouseshare a family but are not interchangeable, and repointing a source between them is the case this catches. Queries with no recorded language run anywhere.Lotus.Storage.TypeCastercolumn_infomap now uses:adapter(an%Adapter{}struct) instead of:source_module(a module atom) for dialect-aware type mapping. Callers that buildcolumn_infomaps — e.g. customTypeHandlerusers — must pass the resolved adapter struct.
Cache
Cache tag prefix renamed —
"repo:<name>"→"source:<name>". Pre-v1 cached entries (discovery + result) won't be found after upgrade; stale entries miss and re-seed on the next read (not a correctness issue). Middleware and customLotus.Cache.KeyBuilderimplementations that tag cache entries must update their prefix.Lotus.Cache.KeyBuilderis now a behaviour.discovery_key/2result_key/4callbacks plus a publicscope_digest/1utility. Configure viacache: %{key_builder: MyApp.KeyBuilder}. Default implementation (Lotus.Cache.KeyBuilder.Default) preserves existing key generation logic (#195).
Scope-aware result cache keys.
result_key/4accepts an optionalscopeparameter (defaultnil). When non-nil, the scope digest is appended to the result cache key and a"scope:<digest>"tag is added to the cache entry.Lotus.invalidate_scope/1clears both discovery and result cache entries for the given scope (#196).Optional
table_stats/3callback.Lotus.get_table_stats/3asks the adapter first and only falls back toSELECT COUNT(*)when the adapter does not implement it. Non-SQL sources can now answer with their engine's own statistics, and may return keys beyond:row_count; the return type widened from%{row_count: non_neg_integer()}tomap().Lotus.run_statement/3and the sharedoptstype are honest. The spec claimedbinary()statements while non-SQL adapters take any term; it now usesLotus.Query.Statement.body/0andparams/0. Theoptstype gained:scopeand:sorts, which the code already read but never declared, and:repoaccepts a module as well as a name.The SQL-shaped callbacks became optional, with defaults.
list_schemas/1,resolve_table_namespace/3,default_schemas/1,builtin_schema_denies/1,quote_identifier/2,apply_filters/3,apply_sorts/3,query_plan/3,supports_feature?/2,db_type_to_lotus_type/2andeditor_config/1all have safe defaults now, so a non-SQL adapter implements the ten callbacks it actually needs instead of thirty, most of them stubs. Existing adapters are unaffected — an implemented callback is still used.%{adapter: MyAdapter, ...}is the canonical data source entry. The named module is used directly, with nocan_handle?/1probing. When two adapters both claim a non-canonical entry, resolution now raises and names them instead of silently picking whichever came first.An unresolvable data source is an error, not the default source. A typo in a saved query's
data_source, or a source dropped from config, used to fall through and run the query against the default database.Lotus.Source.resolve!/2raises and the resolver returns{:error, :not_found}. Only a caller that names no source at all gets the default.%Lotus.Query.Statement{}params may be a map. Positional binds stay a list; engines with named binds carry%{"since" => ~D[2026-01-01]}rather than inventing an order.Lotus.Source.Adapter.feature/0documents the feature atoms thatsupports_feature?/2is asked about::schema_hierarchy,:search_path,:arrays,:jsonand:make_interval.Lotus.Source.Adapters.Ecto.Dialectis public. It was@moduledoc falsewhile the adapter guide told external libraries to implement it; it now carries documentation and ships in the generated docs.set_statement_timeout/2andset_search_path/2moved to@optional_callbacks— engines with no session timeout or search path drop their no-op clauses.
Middleware and telemetry
Payload key
:repo→:sourceacross every middleware event (:before_query,:after_query,:after_list_schemas,:after_list_tables,:after_describe_table,:after_list_relations,:after_discover). The duplicate:repo_namekey was removed from discovery event payloads. Modules that pattern-match on%{repo: _}or%{repo_name: _}must update.Event
:after_get_table_schemarenamed to:after_describe_table— aligns with thedescribe_table/3callback rename.:before_query/:after_querycarry:statement(a%Lotus.Query.Statement{}) instead of separate:sql/:paramskeys. Extract viastatement.body/statement.params.:before_queryplugs can rewrite the statement. Returning{:cont, %{payload | statement: rewritten}}now changes what executes; the runner used to discard the returned payload. This is what row-level security and tenant predicates need.:before_queryconsequently runs before statement sanitization and preflight, so the rewritten statement is the one checked — a plug cannot rewrite its way onto a denied table. Plugs that return the payload untouched are unaffected.Telemetry
[:lotus, :query, :start | :stop | :exception]metadata carries:sourceand:statement. The source name moved from:repoto:source, matching the middleware payloads; handlers that indexed on:repo,:sqlor:paramsmust switch.:contextis also present (caller-supplied opaque value, threaded from therun_query/2+run_statement/3options, #175).The AI layer carries the caller's actor.
Lotus.AI.generate_query/1,generate_query_with_context/1,explain_query/1andsuggest_optimizations/1accept:contextand:scope, and thread them into every query and introspection call the AI makes.Lotus.AI.Tool.from_action/2gained a:contextoption, which becomes the second argument to the action'srun/2;Lotus.AI.Action.actor_opts/1turns it back into the:context/:scopeoptions theLotusfunctions take. Previously every AI-initiated action reached middleware and the visibility resolver with no actor, so the AI could see more than the user it was acting for. Custom actions that ignore theircontextargument are unaffected.AI map keys renamed from SQL-specific names.
Lotus.AI.generate_query/1andgenerate_query_with_context/1return:statementinstead of:sql;Lotus.AI.explain_query/1takes:statementinstead of:sql;Lotus.AI.Conversationmessages carry:statement;Lotus.AI.ErrorDetector.analyze_error/4returns:failed_statement. The values are unchanged — only the key names, which described SQL on a surface that is no longer SQL-only.
Visibility
Scoped visibility is enforced at execution, not only in the explorer.
Lotus.Preflight.authorize/4takes the caller's:scopeand passes it to the visibility resolver, and the runner threads:scopefromLotus.run_query/2andLotus.run_statement/3into both preflight and column policies. Previously a resolver that denied a table for one tenant hid it from the schema browser while a query against it still returned rows. Resolvers that ignore scope (including the shippedLotus.Visibility.Resolvers.Static) behave exactly as before.Lotus.Visibility.Resolvercallbacks gained ascopeargument:schema_rules_for/2,table_rules_for/2,column_rules_for/2. Existing custom resolvers must accept (and may ignore) the argument. The shippedLotus.Visibility.Resolvers.Staticignores scope, so static-config users are unaffected.
AI
Prompts compose from
ai_context— hardcoded dialect branches gone.Lotus.AI.Prompts.QueryGenerationandLotus.AI.Prompts.Optimizationdropped theirdatabase_specific_notes(:postgres | :mysql | :sqlite)switch. The prompts assemble in a fixed order: core role + read-only / workflow instructions + Lotus template DSL rules ({{var}},[[...]], list expansion — language-agnostic, emitted vialotus_template_notes/0)- adapter
syntax_notes(filtered to a generic fallback for untrusted adapters) + adapterexample_query+ response-contract examples. Core content precedes adapter content so an untrusted adapter can't override the Lotus DSL rules via later text.
- adapter
Module renames (no aliases; internal but any host reaching into them must update):
Lotus.AI.SQLGenerator→Lotus.AI.QueryGeneratorLotus.AI.Prompts.SQLGeneration→Lotus.AI.Prompts.QueryGenerationLotus.AI.Actions.GetTableSchema→Lotus.AI.Actions.DescribeTable(LLM-visible tool name changed from"get_table_schema"to"describe_table").Lotus.AI.Actions.ExecuteSQL→Lotus.AI.Actions.ExecuteStatement(tool name"execute_sql"→"execute_statement").Lotus.AI.Actions.ValidateSQL→Lotus.AI.Actions.ValidateStatement(tool name"validate_sql"→"validate_statement").Lotus.AI.QueryGenerator.generate_sql/2→generate_statement/2; itssql_responsetype →statement_response.Lotus.AI.Prompts.QueryGeneration.extract_sql/1→extract_statement/1.
:sqlkeys renamed to:statementacross the AI surface, finishing the v1 move away from assuming every source is SQL:- The
execute_statementandvalidate_statementtool parameter the LLM fills is nowstatement, notsql. ExecuteStatement's result map carries:statementinstead of:sql.QueryGeneration.extract_response/1returns%{statement: ..., variables: ...}instead of%{sql: ...}.Lotus.AI.explain_query/1documents its required option as:statement, which is what the code has always read — the:sqlin the docs was wrong. LikewiseLotus.AI.generate_query/1's result key is:statement, not theresult.sqlthe docs showed.
- The
Prompt content moved from core to adapters. Core's generation prompt no longer ships SQL-specific guidance ("use JOINs for multi-table queries", "add LIMIT for safety", "never generate INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, TRUNCATE") to every source, including non-SQL ones whose write paths those keywords do not name. Core now owns prompt structure — the workflow, the tool list, the
UNABLE_TO_GENERATEprotocol, the fence — and adapters own content about their own language. This follows the enforcement:sanitize_query/3is already an adapter callback, so the adapter decides what counts as a write.ai_context/1gains two optional keys, both capped at 1024 bytes and both stripped for adapters outside:trusted_source_adapters::generation_notes— how to shape a good query for this source.:read_only_notes— which operations this source treats as writes.
Adapter notes render in place of core's defaults, not appended after them. When a field is absent or stripped, core falls back to its own generic text and never to an empty string — an empty
:read_only_noteswould otherwise leave the prompt with no read-only instruction at all.The statement fence is labelled with the adapter's language family (
sqlforsql:postgres,jsonforjson:elasticsearch) instead of alwayssql. The extractor accepts any label, so a new family needs no change in core. The label is taken only from the sanitizedai_context.languageand is re-validated before interpolation.Lotus.AI.suggest_optimizations/1andLotus.AI.QueryOptimizer.suggest_optimizations/2take:statementinstead of:sql. The:statementoption accepts a%Lotus.Query.Statement{}. Drops the:paramsand SQL-string inputs — callers wrap their SQL viaLotus.Query.Statement.new/2.AI functions return
{:error, {:ai_feature_unsupported, feature, reason}}when a capability is disabled.Lotus.AI.generate_query/1,generate_query_with_context/1,suggest_optimizations/1, andexplain_query/1checkai_context.capabilitiesbefore invoking the model. Sources declaringoptimization: {false, reason}now fail fast at the AI entry point rather than surfacing a downstream error.Optimization suggestion type enum changed.
@valid_typesinLotus.AI.Prompts.Optimizationchanged from~w(index rewrite schema configuration)to~w(index rewrite structure configuration). LLM output contract changed — JSON responses emitting{"type": "schema", ...}are invalid. Host UIs rendering suggestion-type labels must add a"structure"case.Lotus.AI.Conversation.schema_contextstruct field →source_context. Accessorupdate_schema_context/2renamed toupdate_source_context/2. Host code reaching intoconversation.schema_contextmust migrate.schema_contextparameter →source_contextinLotus.AI.ErrorDetector.analyze_error/4+suggest_fixes/4,Lotus.AI.Prompts.Explanation.user_prompt/2+fragment_prompt/3, andLotus.AI.Prompts.Optimization.user_prompt/3. Rendered prompt section heading"## Schema Context"→"## Source Context".Lotus.AI.ErrorDetector.analyze_error/4gained an optional 4thai_contextargument. When present, adapter:error_patternsare matched against the error message and each matching pattern's:hintis prepended to the suggestions list. Untrusted adapters have their patterns stripped upstream to[].AI actions dispatch through the adapter contract.
Lotus.AI.Actions.ValidateSQLcallsAdapter.validate_statement/3;Lotus.AI.Actions.DescribeTableandGetColumnValuescallAdapter.parse_qualified_name/2+Adapter.validate_identifier/3. Action names and tool schemas unchanged; only the dispatch path differs.
Module reorganization
Lotus.SQL.*→Lotus.Source.Adapters.Ecto.SQL.*. Internal SQL-specific modules relocated out of the universal code path:FilterInjector,SortInjector,Transformer,Sanitizer,Validator,Identifier. Universal code reaches the same functionality throughLotus.Source.Adaptercallbacks (validate_statement/3,validate_identifier/3,parse_qualified_name/2,apply_filters/3,apply_sorts/3).Lotus.SQL.Transformer.transform/2was split intostrip_quoted_variables/1,transform_wildcards/2, andtransform_pg_intervals/1; custom dialects implementtransform_statement/1composed from those helpers. The oldLotus.SQL.*paths no longer exist.Lotus.SQL.OptionalClause→Lotus.Query.OptionalClause(elevated, not hidden). The[[ ... ]]/{{var}}template syntax is language-agnostic — SQL, JSON DSLs, Cypher, any textual format — so it lives in the universal namespace now. Adapters with AST representations apply this before serialization.FilterInjector.apply/5(shared helper called by dialects) acceptsparams(existing parameter list) andplaceholder_fn(database-specific placeholder generator), returns a{sql, params}tuple.
Supervisor
Lotus.Supervisor.start_link/1registers under the fixed nameLotus.Supervisorby default and collapses{:error, {:already_started, pid}}into{:ok, pid}. Host applications that started multiple unnamed Lotus supervisors in the same BEAM will now see the first call succeed and subsequent calls return the existing supervisor's pid. Passsupervisor_name:to run multiple named instances.
Requirements
- Minimum Elixir version raised from 1.17 to 1.18, and minimum Erlang/OTP raised from 26 to 27. CI now covers Elixir 1.18 / 1.19 / 1.20 across OTP 27–29. Elixir 1.17 and OTP 26 are no longer supported; apps on those versions must upgrade their toolchain before moving to v1.0.
Added
Cache entry options are read from config.
:max_bytes,:compressand:lock_timeoutwere declared inLotus.Config.cache_config/0but nothing read them from the application environment: the first two worked only as a per-call:cacheoption and:lock_timeoutwas unreachable from every caller, so it was always 10 seconds. They are deployment policy, so they now come fromconfig :lotus, cache: %{...}, and a per-call:cacheoption still overrides them. New:Lotus.Config.cache_entry_options/0andLotus.Cache.build_options/2, which replaces the two private copies that had drifted apart.Renamed config keys now fail loudly.
:ecto_repo,:data_reposand:default_repowere dropped silently by the config loader, so an app upgrading from 0.16 was told:storage_repowas missing rather than that the key had moved. Lotus now raises and names every renamed key it finds.A stale
data_repoattribute is rejected.Lotus.create_query/1andupdate_query/2used to drop the unpermitted key and save the query with no source, which then resolved to the default source at run time — a multi-source host silently queried the wrong database. The changeset now returns an error on:data_sourceinstead.Bound query variables in middleware payloads.
:before_queryand:after_queryplugs receive a:varskey with the merged variable map (defaults plus caller-supplied values, keyed by variable name), or%{}for a rawLotus.run_statement/3. Plugs can enforce rules on the values a user picked — maximum date ranges, tenant checks — without parsing the statement. See the middleware guide for a date-range limit example (#97).Lotus.Config.t()lists every configuration key, including:allow_unrestricted_resources,:ai,:source_adapters,:trusted_source_adapters,:source_resolver,:visibility_resolverand:middleware.An empty middleware config clears the compiled pipeline —
Lotus.Middleware.compile/1used to ignore an empty or missing config, so a config reload could add middleware but never take it away. It now erases the compiled pipeline, which also lets a host app (or a test) turn middleware off at runtime.:dynamic_optionsfeature atom —supports_feature?/2now answers a documented:dynamic_optionsquestion: whether a query against this source can return a flat list of values suitable for populating a variable's dropdown. Every built-in SQL dialect (Postgres, MySQL, SQLite, and the generic Ecto fallback) answerstrue; adapters whose query language returns shaped documents rather than rows answerfalse, and the dashboard then offers manual option entry only (elixir-lotus/lotus_web#127).editor_config/1exposes two optional extension points —:dialect_spec(SQL tokenizer options: identifier quotes, operator chars, hash / slash / dollar-quoted string rules, PL/SQL quoting, etc., forwarded verbatim to CodeMirror'sSQLDialect.define()) and:context_schema(structural JSON DSL completion schema: per-parent valid keys, marker atoms for field-name / nested-query / named- aggregation lookups, and value-literal lists). External SQL adapters declaring:dialect_specreach tokenization parity with the built-in PG/MySQL Lezer grammars; JSON DSL adapters (Elasticsearch, future OpenSearch) declare a:context_schemathat drives parent- aware autocomplete inlotus_web. Mirroring theai_contextsanitization philosophy,editor_config/1payloads are capped at theLotus.Source.Adapterdispatch layer::keywordsand:typesat 2000 entries each,:functionsat 500,:context_schema.rootat 200,:context_schema.childrenat 500, unknown top-level keys dropped, with a one-timeLogger.warning/1per(adapter, field)truncation (deduped via:persistent_term) so a noisy or compromised adapter can't ship a huge payload over LiveView to every editor session (elixir-lotus/lotus_web#126).First-party non-SQL reference adapter
Lotus.Test.InMemoryAdapter(intest/support/). Implements the fullLotus.Source.Adaptercontract against an in-memory dataset using a structured DSL map as the%Statement{}payload (%{from, where, order_by, limit, offset}) — no SQL text, no Jason encoding, no driver dependency. Exercisessubstitute_variable/5+substitute_list_variable/5via{:var, name}markers embedded in the:whereclause, and declares per-feature AI capabilities. Serves as both a test fixture and a starting template for external non-Ecto adapters, with coverage intest/lotus/source/adapters/in_memory_adapter_test.exs,test/integration/non_sql/in_memory_end_to_end_test.exs, andtest/lotus/ai/in_memory_adapter_ai_test.exs(35 tests total).Lotus.AI.supports?(source_name, feature)andLotus.AI.unsupported_reason(source_name, feature)— UIs gate AI buttons per-source per-feature, readingai_context.capabilities. Reasons from untrusted adapters are replaced with a generic fallback at the dispatch layer.Lotus.UnsupportedOperatorErrorexception raised when a filter operator is not in the adapter's declared support list.:scopeoption on all discovery functions (list_schemas/2,list_tables/2,describe_table/3,list_relations/2,get_table_stats/3). Opaque term passed to the visibility resolver and hashed into the cache key — enables context-aware visibility rules (per-role, per-tenant) with correct per-scope caching. Whennil(the default), cache keys and behavior are identical to pre-scope versions. Discovery middleware payloads include:scope.Per-scope cache invalidation via
Lotus.invalidate_scope/1(delegates toLotus.Cache.invalidate_scope/1). Selectively clears all cached entries associated with a specific scope without flushing the entire cache, using tag-based invalidation (#195).:after_discovermiddleware event fires after any discovery call alongside the kind-specific:after_list_*event. Payload is uniform%{kind:, source:, result:, scope:, context:}. Lets a single middleware module handle every discovery kind by dispatching on:kind(#173).Middleware exception safety — raised exceptions inside middleware
call/2are caught and surfaced as{:error, exception}instead of propagating uncaught. The rescue is scoped to the individualcall/2invocation so subsequent middleware never runs (#177).:preloadoption onLotus.Dashboards.list_dashboards/1andlist_dashboards_by/1for eager-loading associations (e.g.:cards) in a single query. Fixes N+1 patterns in callers that need card counts or card lists alongside the dashboard list (elixir-lotus/lotus_web#103).Documentation — new guide
guides/upgrading-to-v1.mdwith a 10-step upgrade checklist covering every breaking change in this release. Rewrittenguides/source-adapters.mdto the v1.0 contract. Newguides/custom-resolvers.mdforLotus.Source.ResolverandLotus.Visibility.Resolverextension points (#176).Lotus.TaskSupervisoradded to the supervision tree. Dashboard card execution usesTask.Supervisor.asyncinstead of bareTask.async, giving proper OTP supervision and fault tolerance (#169).Typespecs on
Lotus.Cachepublic API for Dialyzer coverage against the cache facade (#161).
Changed
Schema-introspection telemetry emits
:source, not:repo. The[:lotus, :schema, :introspection, :*]events carried the source name under:repowhile the query events used:sourcefor the same value. Handlers matching on:repomust be updated.The guides were rewritten against the v1 code. Every guide was audited callback by callback and key by key: the configuration, caching and introspection guides still taught pre-v1 config keys and removed functions, the adapter guide's callback table did not match the behaviour, and several examples could not have run.
guides/schema-introspection.mdnow ships with the docs; it was never listed in the extras before.:sha256column masks hash a rendered form of values that are neither text nor binary, so digests change for those columns: aNaiveDateTimenow hashes"2024-01-01T00:00:00"rather than"2024-01-01 00:00:00", and an array hashes its JSON form rather than a charlist. Text and binary columns hash exactly the bytes they did before, andDecimalrenders identically either way. Re-key anything that stores or joins on these digests.Lotus.Config.load!/0caches the validated config in:persistent_terminstead of re-runningNimbleOptions.validate/2on every accessor call.Lotus.Config.reload!/0refreshes the cached value (called fromLotus.Supervisor.init/1at boot, and available to tests that mutateApplicationenv).load!/1with explicit opts still validates without touching the cache (#178).Lotus.can_run?/2now reuses the privateprepare_variables/2helper instead of duplicating default-merge logic inline (#156).Shared filter → sort → pagination → cache → execute pipeline extracted from
Lotus.run_statement/3andLotus.run_query/2into a single privateexecute_with_options/7helper (#160).Discovery middleware (
:after_list_*) now runs outside the schema cache callback, so context-sensitive filtering is no longer cached by the first caller's context and served to later callers. Side-effecting middleware that undercounted by running only on cache misses will now run on every call (#173).Discovery middleware that raises now propagates the exception instead of being silently converted to
{:error, message}(matching:before_query/:after_querybehaviour inLotus.Runner) — middleware should return{:halt, reason}for error conditions, not raise (#173).guides/middleware.mddocumented the:after_describe_tablepayload key as:table_schema, butLotus.Schemaactually sends:columns(plus the previously-undocumented:table_nameand:schemakeys). Documentation now matches the code (#173).
Fixed
Normalizing a
Decimalno longer depends on which Decimal an application resolved.Lotus.Normalizerrendered one withDecimal.to_string/3andmax_digits: :infinity, so a numeric wider than Decimal's default output cap renders in full instead of raising. That arity arrived in Decimal 2.4.0, and Lotus pins no Decimal of its own, so an application on an older one got anUndefinedFunctionErrorthe moment a query returned a numeric column. The impl now picks its arity at compile time, the wayLotus.JSONpicks its JSON library. Both branches render the same output, since the versions without the option have no cap to lift.Lotus.Result.Statisticsnormalizes string values throughLotus.Value.to_display_string/1before measuring them. Columns whose values are not valid UTF-8 (PostgreSQLbytea, for example) raisedArgumentErrorfromString.length/1; they now report the Base64 form the UI and exports already show,:min_lengthand:max_lengthare always grapheme counts of that displayed form, and:top_valuesis always safe to JSON-encode. Raw 16-byte UUID binaries report as UUID strings rather than aninspect/1representation.Column masking renders values that are neither text nor binary instead of failing the query. A
:sha256or{:partial, opts}mask on ajsonbcolumn raisedprotocol String.Chars not implemented for type Map, which surfaced as a query error rather than a masked row. Such values now go throughLotus.Value.to_display_string/1, the same rendering the UI and exports use.describe_table/3andget_table_stats/3now propagate adapter errors (permission denied, connection errors) instead of masking them as "Table not found" (#189).Lotus.Storage.Query.compile/2uses falsy supplied values (false,0) correctly instead of short-circuiting through||and falling back to the variable's default.nilsupplied values still fall back to the default (#163).Lotus.NormalizerforURInow renders URIs as URL strings viaURI.to_string/1instead ofinspect/1, which produced struct representations (%URI{...}) (#159).Lotus.Config.cache_namespace/0returns a consistent default regardless of cache configuration state. The previous implementation returned"lotus:v0"when no cache was configured and"lotus:v1"when a cache was configured without an explicit namespace (#165).Lotus.NormalizerforDecimalrenders values of any width instead of raisingArgumentError. Decimal 3.0 capsDecimal.to_string/2output at 6178 digit characters, but query results can exceed that (an unconstrained PostgreSQLnumericallows 131072 integer digits). The error escapedLotus.Result.to_encodable/1and everyLotus.Export.Valuepath; the implementation now passesmax_digits: :infinity.
Security
- The
{:partial, opts}column mask no longer returns a value in full when:keep_firstplus:keep_lastcovers its whole length. A four-character value underkeep_last: 4came through unmasked; such a value is now masked completely. - The
{:partial, opts}column mask hides every byte of a value that is not valid UTF-8 instead of measuring it as text.String.length/1andString.slice/1report unreliable lengths for such binaries, which let abyteavalue pass through the mask intact. - Filter values are parameterized (bound as
$1,?) instead of string-interpolated, eliminating SQL-injection risk via crafted filter values (#152). - Column names in
FilterInjectorandSortInjectorare validated against[a-zA-Z_][a-zA-Z0-9_]*viaLotus.Source.Adapters.Ecto.SQL.Identifier, rejecting names with spaces, quotes, semicolons, or other special characters (#152). - Nested block-comment depth is tracked in
Runner'sskip_block_comment/1so the single-statement parser matches PostgreSQL's nested block-comment semantics. The previous implementation exited at the first*/, which could let a second statement slip pastassert_single_statement/1when hidden inside a nested comment (#164). Lotus.Config.cache_namespace/0now returns a consistent"lotus:v1"default regardless of whether a cache is configured, eliminating an inconsistency where the un-configured path returned"lotus:v0"(#165)Lotus.Normalizerimplementation forURInow usesURI.to_string/1instead ofinspect/1, producing the actual URL string rather than the%URI{}struct representation (#159)Propagate
Repo.transaction/1errors fromDashboards.reorder_dashboard_cards/2instead of unconditionally returning:ok. Spec updated to:ok | {:error, term()}(#157)- Use
Task.Supervisorinstead of bareTask.asyncfor dashboard card execution, ensuring proper OTP supervision and fault tolerance. AddedLotus.TaskSupervisorto the supervision tree. - Cache validated
Lotus.Configin:persistent_termto avoid repeatedNimbleOptions.validate/2on every accessor call. Config is eagerly validated once at boot fromLotus.Supervisor.init/1; a newLotus.Config.reload!/0refreshes the cached value when the application environment changes (e.g. in tests) (#154) - Clarify in the installation and caching guides that Lotus's supervisor starts automatically with the
:lotusOTP application — consumers do not need to addLotusto their own supervision tree to enable caching - Add
@specannotations to all public functions inLotus.Cache(get/1,put/4,get_or_store/4,delete/1,invalidate_tags/1,enabled?/0) to improve discoverability and Dialyzer coverage (#161) guides/middleware.mddocumented the:after_get_table_schemapayload key as:table_schema, butLotus.Schemaactually sends:columns(plus the previously-undocumented:table_nameand:schemakeys). Middleware written to the documented contract would have raisedKeyError. Doc now matches the code (#173)- Discovery middleware (
:after_list_*) previously ran inside the schema cache callback, so context-sensitive filtering was cached by the first caller's context and served to later callers with different contexts. The middleware pipeline now runs outside the cache; only the raw, visibility-filtered adapter result is cached. Side-effecting middleware (e.g. audit logging) that previously undercounted by logging only on cache misses will now run on every call — adjust if this change in volume matters for your use case (#173) - Discovery middleware that raises an exception now propagates the exception to the caller instead of being converted to
{:error, message}. The previous conversion was an incidental side-effect of an adapter-leveltry/rescuethat wrapped the middleware pipeline; after the cache refactor above, middleware runs outside that rescue. This matches the existing behavior of:before_query/:after_querymiddleware inLotus.Runner. Middleware should return{:halt, reason}for error conditions, not raise (#173)
Dependencies
Security advisories resolved by these bumps:
- Bumped
decimalfrom 2.4.1 to 3.1.1 — CVE-2026-32686 (unbounded exponent enables unauthenticated DoS). Major release:parse/1andcast/1now reject inputs wider than 34 digits, returning:error(Lotus.Storage.TypeCastersurfaces this as a cast error), and the default context precision moves from 28 to 34. - Bumped
mintfrom 1.7.1 to 1.10.0 — CVE-2026-48861, CVE-2026-48862, CVE-2026-49753, CVE-2026-49754, CVE-2026-56810, CVE-2026-58229, CVE-2026-59246, CVE-2026-59249, CVE-2026-82728 (response smuggling, HTTP/2 CONTINUATION floods, unbounded buffering). - Bumped
postgrexfrom 0.22.0 to 0.22.4 — CVE-2026-32687, CVE-2026-58225, CVE-2026-66838 (SQL injection via notification channel name, the:commentoption, and dollar-quote replay). - Bumped
reqfrom 0.5.17 to 0.7.4 — CVE-2026-49755, CVE-2026-49756 (decompression-bomb DoS, multipart header injection). - Bumped
hpaxfrom 1.0.3 to 1.0.4 — CVE-2026-58226 (unbounded HPACK integer decoding).
Supporting bumps required to reach the versions above:
- Bumped
ectofrom 3.13.5 to 3.14.2 - Bumped
ecto_sqlfrom 3.13.5 to 3.14.0 - Bumped
ecto_sqlite3from 0.22.0 to 0.24.1 - Bumped
myxqlfrom 0.8.2 to 0.9.0 - Bumped
req_llmfrom 1.11.0 to 1.22.0 - Bumped
telemetryfrom 1.4.1 to 1.4.2 - Transitively bumped
db_connectionto 2.10.2,elixir_maketo 0.10.0,exqliteto 0.40.0,finchto 0.23.0,jsvto 0.22.0,llm_dbto 2026.9.1,server_sent_eventsto 1.1.0,splodeto 0.3.2,textureto 1.2.1, andzoito 0.18.7
No mix.exs requirements changed; every update fits the existing
version constraints.
[0.16.4] - 2026-03-10
Fixed
- FIX: Remove
@derive {Lotus.JSON.encoder(), ...}fromResultstruct that caused{:invalid_byte, 255}crashes when query results contained raw UUID binaries from PostgreSQL. AddedResult.to_encodable/1for explicit JSON-safe serialization with value normalization. Regression introduced in v0.16.0 (#135)
Changed
- REFACTOR: Extract
Lotus.Normalizerprotocol fromLotus.Export.Normalizerinto a top-level module for general-purpose value normalization (UUID binaries, Dates, Decimals, Postgrex/MyXQL types).Lotus.Export.Valuenow delegates toLotus.Normalizer.Lotus.Export.Normalizerhas been removed — if you implemented this protocol for custom types, implementLotus.Normalizerinstead. - NEW: Added
guides/middleware.mddocumentation for the middleware pipeline
[0.16.3] - 2026-03-08
Fixed
- FIX: Accept date-only strings (e.g.
"2025-07-01") when casting to:datetimetype — theTypeCasternow falls back to parsing as aDateand converts to midnight (~N[2025-07-01 00:00:00]) instead of raising "Invalid datetime format". This fixes a regression where query variables typed as:datewere overridden by auto-detected:datetimecolumn types from the database
[0.16.2] - 2026-03-08
Fixed
- FIX: Strip trailing semicolons from SQL queries before wrapping them in CTEs in
FilterInjectorandSortInjector— queries ending with;(e.g. CTE queries) would trigger the "Only a single statement is allowed" error when filters or sorts were applied - NEW: Extract shared
Lotus.SQL.Sanitizermodule for SQL string cleanup helpers used by injectors
[0.16.1] - 2026-03-08
Fixed
- FIX: Wrap raw LLM provider errors in
Lotus.AI.Errordomain exceptions before returning them to callers — raw errors from ReqLLM (containing API endpoints, stack traces, provider metadata) are now logged server-side and replaced with user-safe error structs:RateLimitError,AuthenticationError,ServerError,TimeoutError,ServiceError
[0.16.0] - 2026-03-08
Added
- NEW: Middleware pipeline for query execution and schema discovery hooks (
Lotus.Middleware)- Plug-style
init/1+call/2callbacks with{:cont, payload}/{:halt, reason}control flow - Query events:
:before_query,:after_query - Discovery events:
:after_list_schemas,:after_list_tables,:after_get_table_schema,:after_list_relations - Compiled to
:persistent_termat startup for zero-overhead runtime dispatch - Opaque
:contextoption allows user data to be provided to middleware (e.g. current user) through all middleware
- Plug-style
- NEW: Result filtering via
:filtersoption onLotus.run_query/2andLotus.run_sql/3- Pass a list of
Lotus.Query.Filterstructs to apply WHERE conditions on top of any query - Filters are applied by wrapping the original query in a CTE, so they work safely with any SQL complexity (joins, subqueries, unions, etc.)
- Supports operators:
=,!=,>,<,>=,<=,LIKE,IS NULL,IS NOT NULL - Source-aware: each database adapter (PostgreSQL, MySQL, SQLite) handles its own identifier quoting via new
quote_identifier/1andapply_filters/2callbacks onLotus.Source - New
Lotus.Query.Filterstruct for source-agnostic filter representation - New
Lotus.SQL.FilterInjectorshared helper for SQL-based sources
- Pass a list of
- NEW: Result sorting via
:sortsoption onLotus.run_query/2andLotus.run_sql/3- Pass a list of
Lotus.Query.Sortstructs to apply ORDER BY on top of any query - Sorts are applied by wrapping the original query in a CTE, so they work safely with any SQL complexity (joins, subqueries, unions, existing ORDER BY, etc.)
- Supports
:ascand:descdirections - Source-aware: each database adapter handles its own identifier quoting via new
apply_sorts/2callback onLotus.Source - New
Lotus.Query.Sortstruct for source-agnostic sort representation - New
Lotus.SQL.SortInjectorshared helper for SQL-based sources
- Pass a list of
- FIX: AI SQL generation now validates plain SQL responses (without
```sqlcode blocks) against the database using EXPLAIN before rejecting them — valid SQL is accepted, conversational text is still rejected as{:error, {:unable_to_generate, content}}(#127) - NEW:
Lotus.SQL.Validator— validates SQL syntax against the database without executing, using EXPLAIN. Neutralizes{{var}}and[[...]]template syntax before validation - NEW:
Lotus.AI.Actions.ValidateSQL— AI tool action that lets the LLM validate its SQL against the database before returning it - NEW:
Lotus.Variables— universal utilities for{{variable}}template syntax:regex/0,extract_names/1,neutralize/2. Consolidates the variable regex previously duplicated acrossOptionalClause,Query, andQueryOptimizer - NEW:
Lotus.SQL.OptionalClause.strip_brackets/1— strips[[/]]brackets unconditionally, keeping inner content. Used byValidatorandQueryOptimizerfor preparing SQL for EXPLAIN - FIX:
Lotus.Source.param_placeholder/4andLotus.Source.limit_offset_placeholders/3no longer hardcode a fallback to PostgreSQL when the repo isnil— they now resolve via the configured default data repo - NEW: Optional variables with [[ ]] syntax
- NEW: Column-level statistics for query results (
Lotus.Result.Statistics)- Computes per-column statistics from in-memory result sets without additional database queries
- Numeric columns: min, max, avg, median, sum, distinct count, null count/percentage, histogram (10 bins)
- String columns: distinct count, top values with counts, null count/percentage, min/max length
- Temporal columns: earliest, latest, null count/percentage, distribution over time
- Supports
Date,DateTime,NaiveDateTime,Time,Decimal, and standard Elixir types - Public API:
compute/2(single column),compute_all/1(all columns),detect_column_type/2
- NEW: Telemetry integration for observability
- Query execution events:
[:lotus, :query, :start],[:lotus, :query, :stop],[:lotus, :query, :exception] - Cache operation events:
[:lotus, :cache, :hit],[:lotus, :cache, :miss],[:lotus, :cache, :put] - Schema introspection events:
[:lotus, :schema, :introspection, :start],[:lotus, :schema, :introspection, :stop] - New
Lotus.Telemetrymodule with event reference documentation - Telemetry guide with setup instructions and LiveDashboard integration example
- Query execution events:
- NEW: AI-powered query optimization suggestions (
Lotus.AI.suggest_optimizations/1)- Analyzes SQL queries and execution plans to suggest performance improvements
- Returns categorized suggestions with type (index/rewrite/schema/configuration) and impact level (high/medium/low)
- Uses EXPLAIN plan analysis combined with AI to provide actionable recommendations
- Schema-aware: uses
get_table_schematool to inspect relevant tables - Handles Lotus-specific
{{variable}}and[[optional clause]]syntax — sanitizes before EXPLAIN, preserves original SQL for AI analysis
- NEW: AI-powered query explanation (
Lotus.AI.explain_query/1)- Get plain-language explanations of what a SQL query does
- Supports explaining a full query or a selected fragment (e.g., a single JOIN, a HAVING clause)
- Fragment mode sends the full query as context so even isolated terms are explained accurately
- Understands Lotus-specific
{{variable}}and[[optional clause]]syntax and explains their runtime behavior - Schema-aware: uses
get_table_schematool to inspect relevant tables for richer explanations
- NEW:
Lotus.AI.Actionbehaviour andLotus.AI.Tool.from_action/2for declarative AI tool definitions- Define tools as modules with
name/0,description/0,schema/0(NimbleOptions), andrun/2callbacks Tool.from_action/2converts action modules toReqLLM.tool()structs with automatic JSON Schema generation- Supports parameter binding via
:bindoption to hide/pre-fill parameters from the LLM - Built-in actions:
ListSchemas,ListTables,GetTableSchema,GetColumnValues,ListDataSources,ExecuteSQL
- Define tools as modules with
- NEW:
Lotus.AI.Tool.run/4— shared tool-calling loop that replaces duplicated loops inSQLGenerator,QueryOptimizer, andQueryExplainer - NEW:
Lotus.SQL.Identifier— shared module for SQL identifier validation and parsing- Validates identifiers against
[a-zA-Z_][a-zA-Z0-9_]*to prevent SQL injection in interpolated values validate_identifier!/2andvalidate_search_path!/1guard Postgressearch_pathand SQLitePRAGMAinterpolations- Consolidates
parse_table_name/1,validate_identifier/2, andvalidate_table_parts/2previously inLotus.AI.Actions.Helpers
- Validates identifiers against
- Added
JSON.Encoderderive forLotus.Resultstruct
Breaking
- Replaced
langchaindependency withreq_llmfor AI query generation- Removed provider abstraction layer (
Lotus.AI.Providerbehaviour,Lotus.AI.ProviderRegistry, and individual provider modules) - New
Lotus.AI.SQLGeneratormodule replacesLotus.AI.Providers.Core - AI config simplified from separate
provider+modelkeys to a singlemodelkey using ReqLLM's"provider:model"format (e.g.,"openai:gpt-4o","anthropic:claude-sonnet-4-5-20250514") - All providers supported by ReqLLM are now available (OpenAI, Anthropic, Google, Groq, Mistral, and more)
generate_query/1andgenerate_query_with_context/1returnmodel(full model string) instead ofprovider
- Removed provider abstraction layer (
Changed
- Refactored
SQLGenerator,QueryExplainer, andQueryOptimizerto useActionmodules +Tool.from_action/2instead of inline tool construction - Replaced duplicated tool-calling loops in each AI module with shared
Tool.run/4 - Replaced per-module usage normalization with shared
Tool.normalize_usage/1 - Removed
Lotus.AI.Tools.SchemaTools— replaced byLotus.AI.Actions.*modules
[0.15.0] - 2026-03-05
Added
- NEW:
read_only: falseoption forrun_sql— disables the application-level deny list, allowing write queries (INSERT, UPDATE, DELETE, DDL). Single-statement validation and visibility rules still apply. - NEW: AI-generated query variable configurations alongside SQL
- LLM can now produce
{{variable}}placeholders with full variable metadata (type, widget, label, default, list, static_options, options_query) - System prompt teaches the LLM when and how to generate variables (only on explicit user request, never proactively)
- Smart options strategy:
static_optionsviaget_column_values()for small cardinality,options_queryfor dynamic/large sets extract_variables/1parser for```variablesJSON blocks with normalization (type validation, widget/list defaults, nil stripping)extract_response/1unified extractor combining SQL and variable extraction for providers- All providers (OpenAI, Anthropic, Gemini) return
variablesin their response map generate_query/1andgenerate_query_with_context/1now includevariablesin the result- Conversation history preserves and formats variable context across multi-turn exchanges
- LLM can now produce
Changed
Lotus.AI.Provider.responsetype now includes avariablesfieldConversation.add_assistant_response/4accepts an optionalvariablesparameter (defaults to[])- Providers use
SQLGeneration.extract_response/1instead ofextract_sql/1for response parsing - Fixed cache adapter constraint that prevented custom cache adapters from being used
Dependencies
- Bumped
langchainfrom 0.5.2 to 0.6.0 - Bumped
ecto_sqlfrom 3.13.4 to 3.13.5 - Bumped
credofrom 1.7.16 to 1.7.17
[0.14.0] - 2026-02-16
Added
- NEW: List variable support for multi-value query parameters (e.g.,
IN (...)clauses)- Variables with
list: trueexpand to multiple SQL placeholders at execution time - Correct parameter index sequencing when mixing list and scalar variables
- Per-element type casting for list values (e.g.,
:numbercasts each element individually) - Automatic normalization of comma-separated strings into lists (e.g.,
"US, UK, DE"→["US", "UK", "DE"]) - Support for all database adapters (PostgreSQL
$1, $2, $3, MySQL/SQLite?, ?, ?) - Validation that list variables contain at least one value
- Added
listboolean field toQueryVariableschema (defaults tofalse)
- Variables with
[0.13.0] - 2026-02-10
Added
- NEW: Long-running conversation support for AI-powered query generation
- Multi-turn conversations with context retention across messages
- Conversational refinement of generated queries based on user feedback
- Enhanced error detection and query optimization capabilities
- Support for iterative query improvements without starting from scratch
- Replaces previous "fire and forget" single-request model with stateful conversations
Changed
- BREAKING: Minimum Elixir version bumped from 1.16 to 1.17 (required by
langchaindependency)
[0.12.0] - 2026-02-10
Added
- NEW (EXPERIMENTAL): AI-powered SQL query generation from natural language
- Support for OpenAI (GPT-4, GPT-4o), Anthropic (Claude), and Google Gemini models
- Schema-aware query generation with tool-based introspection
- Automatic discovery of schemas, tables, columns, and enum values
- Multi-turn conversations with LLM for complex queries
- Respects Lotus visibility rules - AI sees only what users see
- Configuration-based setup (no database changes required)
Lotus.AI.generate_query/1API for programmatic access- Four introspection tools:
list_schemas(),list_tables(),get_table_schema(),get_column_values() - Provider-agnostic architecture with shared tool implementations
- Disabled by default - requires explicit configuration
- See "AI Query Generation" section in README for setup instructions
Changed
- Added
langchainas a required dependency (needed for AI features) - AI features are opt-in via configuration - no impact if not configured
[0.11.0] - 2026-02-04
Added
- NEW: Dashboard support for combining multiple queries into interactive, shareable views
- Create dashboards with cards arranged in a 12-column grid layout
- Card types: query results, text, headings, and links
- Dashboard-level filters that map to query variables across cards
- Public sharing via secure tokens
- Parallel query execution with configurable timeouts
- ZIP export with CSV per card
- NEW: Automatic type casting system for query variables with intelligent column type detection
- NEW:
Lotus.Storage.TypeHandlerbehaviour for implementing custom database type handlers - NEW: Custom type handler registry system via
Application.get_env(:lotus, :type_handlers) - NEW: Support for complex PostgreSQL types:
- Array types (
integer[],text[], etc.) with element-wise casting - Enum types (
USER-DEFINEDtypes) with pass-through handling - Composite types with JSON input support
- PostgreSQL array format parsing (supports both
{1,2,3}and[1,2,3]formats)
- Array types (
- NEW:
Lotus.Storage.SchemaCachefor caching column type information - NEW:
Lotus.Storage.TypeMapperfor mapping database types to Lotus internal types - NEW:
Lotus.Storage.TypeCasterfor converting string values to database-native formats - NEW:
Lotus.Storage.VariableResolverfor automatic variable-to-column binding detection - Added support for
:timetype with ISO8601 time parsing (e.g., "10:30:00") - Added graceful fallback handling when schema cache is unavailable (defaults to
:texttype) - Added "cast only when needed" optimization - text and enum types pass through without casting
- Added comprehensive logging for type detection failures (debug/warning levels)
- Added support for schema-qualified table names in automatic type detection (e.g.,
public.users) - Added custom type handler validation to ensure handlers implement required callbacks
Changed
- Enhanced query variable system to automatically detect column types from database schema
- Enhanced type casting to prioritize automatic detection for non-text types, falling back to manual types
- Improved error messages for type casting failures with helpful format hints
[0.10.0] - 2026-01-05
Added
- NEW:
Lotus.Cache.Cachexadapter for local or distributed in-memory caching using Cachex - NEW: Query visualization storage with opaque config (validation delegated to consumers)
- NEW:
Lotus.Vizmodule for visualization CRUD operations - NEW: Visualization config validation against query results
- Added
list_visualizations/1,create_visualization/2,update_visualization/2,delete_visualization/1delegations to mainLotusmodule - Added
validate_visualization_config/2for validating visualization configs against result columns - NEW: Column-level visibility rules with masking support (
:allow,:omit,:mask,:error) - NEW:
Lotus.Visibility.Policymodule for policy creation and validation - NEW:
Lotus.Preflight.Relationsmodule for cleaner preflight relation management
Changed
- INTERNAL: Comprehensive Credo-based code quality improvements:
- Eliminated deeply nested functions by extracting helper functions
- Reduced cyclomatic complexity across multiple modules
- Replaced
unless/elsepatterns with cleanerif/elsestructures - Converted single-clause
withstatements to more appropriatecasestatements - Refactored complex
condstatements to use pattern matching - Improved function naming conventions (e.g.,
is_repo_module?→repo_module?) - Configured selective exclusions for
MapJoinwarnings where readability is prioritized - Enhanced code maintainability and testability without changing public APIs
[0.9.2] - 2025-09-07
- Added
Lotus.Export.stream_csv/2to export the full result page by page
[0.9.1] - 2025-09-05
Added
- Added windowed pagination capped at max 1000 pages to prevent performance degradation
[0.9.0] - 2025-09-04
Added
- Added
num_rows,duration_ms, andcommandattributes toLotus.Resultstruct returned by query execution - Added comprehensive error messages for type conversion failures in query variables
- Added support for both integer and float parsing in
:numbertype variables
Changed
- BREAKING: Enhanced
QueryVariable.static_optionsto support multiple input formats but normalize output to%{value: String.t(), label: String.t()}maps
Fixed
- Fixed type casting errors that previously showed generic "Missing required variable" instead of specific type conversion issues
- Fixed number type variables to properly handle both integers (
"123") and floats ("123.45") - Fixed date type variables to show clear error messages for invalid date formats
- Improved error messages to distinguish between truly missing variables and type conversion failures
Data Migration Required
If you have existing queries stored in your database with static_options in the old format, you will need to migrate them. See the Migration Guide in README.md for detailed instructions and migration script.
[0.8.0] - 2025-09-03
Added
- NEW: Two-level schema and table visibility system with schema rules taking precedence over table rules
- NEW: Comprehensive export system with CSV, JSON, and JSONL support for Lotus.Result structs
- Added
schema_visibilityconfiguration for controlling which schemas are accessible through Lotus - Added schema visibility functions to
Lotus.Visibilitymodule:allowed_schema?/2- Check if a schema is visiblefilter_schemas/2- Filter a list of schemas by visibility rulesvalidate_schemas/2- Validate that all requested schemas are visible
- Added
builtin_schema_denies/1callback to Source behaviour for adapter-specific system schema filtering - Added automatic schema visibility filtering to
list_schemasandlist_tablesfunctions - Added implementation of
list_schemasfor all database adapters:- PostgreSQL: Returns actual schema names from
information_schema.schemata - MySQL: Returns database names as schemas
- SQLite: Returns empty list (no schema support)
- PostgreSQL: Returns actual schema names from
- Added comprehensive MySQL adapter tests in
Lotus.SchemaTestcovering all schema introspection functions - Added
Lotus.Exportmodule withto_csv/1,to_json/1, andto_jsonl/1functions for exporting query results - Added protocol-based
Lotus.Export.Normalizersystem for database value normalization with support for:- All basic Elixir types (atoms, numbers, strings, booleans, dates/times)
- Database-specific types (PostgreSQL ranges, intervals, INET, geometric types)
- Binary data handling (UUIDs, Base64 encoding for non-UTF-8 data)
- Collections (maps preserved for JSON, stringified for CSV)
- Decimal types with proper NaN/Infinity handling
- Added battle-tested UUID binary handling using
Ecto.UUID.load/1 - Added comprehensive test coverage for all export functionality and edge cases
- Added NimbleCSV integration for robust CSV generation with proper escaping
- Added central
Lotus.Valuemodule providing unified interface for value normalization across JSON/CSV/UI contexts
Changed
- BREAKING: Renamed
Lotus.QueryResulttoLotus.Resultfor cleaner API naming with the introduction ofLotus.Value
Fixed
- Fixed SQL quoting in
get_table_statsto use adapter-specific quote characters (backticks for MySQL, double quotes for PostgreSQL) - Fixed MySQL builtin_denies to properly filter system tables with database-specific schema names
- Fixed PostgreSQL and MySQL schema tests to correctly expect errors (not empty results) for non-existent tables
[0.7.0] - 2025-09-01
Added
- NEW: Comprehensive caching system with adapter behaviour and ETS backend
- NEW: OTP application and supervisor support for production deployment
- Added
Lotus.ApplicationandLotus.Supervisorfor managed cache backend lifecycle - Added
Lotus.child_spec/1andLotus.start_link/1for supervision tree integration - Added
Lotus.Cachebehaviour for implementing custom cache adapters - Added
Lotus.Cache.ETSadapter providing in-memory caching with TTL support - Added cache configuration with predefined profiles (
:results,:options,:schema) that ship with built-in defaults and support custom TTL strategies - Added cache namespace support for multi-tenant applications
- Added tag-based cache invalidation for targeted cache clearing
- Added cache modes: default caching,
:bypass(skip cache),:refresh(update cache) - Added cache key generation based on SQL, parameters, repository, search path, and Lotus version
- Added cache integration for both
run_sql/3andrun_query/2functions - Added cache integration for all Schema functions (
list_tables/2,get_table_schema/3,get_table_stats/3,list_relations/2) - Added cache options passing (
max_bytes,compress) through the API layer - Added built-in cache profile defaults:
:results(60s TTL),:schema(1h TTL),:options(5m TTL) - available without any configuration
Enhanced
- Enhanced
run_sql/3andrun_query/2to automatically use configured cache when available - Enhanced all Schema functions with read-through caching using appropriate profiles (
:schemafor metadata,:resultsfor statistics) - Enhanced cache system with automatic adapter detection and graceful fallback when no adapter configured
- Enhanced cache configuration with profile-specific TTL settings and runtime overrides
- MAJOR: Refactored schema introspection system to be completely database-agnostic with proper caching of table schema resolution queries
Changed
- BREAKING: Renamed
Lotus.Adapterbehaviour toLotus.Sourcein preparation for caching functionality and to support future non-SQL data sources - BREAKING: Renamed adapter modules from
Lotus.Adapters.*toLotus.Sources.*(Lotus.Sources.Postgres,Lotus.Sources.MySQL,Lotus.Sources.SQLite3,Lotus.Sources.Default) - BREAKING: Renamed
Lotus.SourceUtilsmodule toLotus.Sourcesand expanded its functionality to include data source registration, dynamic module resolution, and comprehensive source management utilities - INTERNAL: Moved all database-specific schema operations (
list_tables,get_table_schema,resolve_table_schema) fromLotus.Schemato respective source modules for better separation of concerns - INTERNAL:
Lotus.Schemais now completely database-agnostic and delegates all DB-specific operations to source modules - PERFORMANCE: Added caching to
resolve_table_schemaqueries to eliminate the expensive "which schema is this table in?" database lookups that were being repeatedly executed
Fixed
- Fixed schema introspection for SQLite databases by properly handling schema-less database architecture (empty schemas list instead of
["public"])
[0.6.0] - 2025-08-31
Added
- Added
Lotus.can_run?/1andLotus.can_run?/2functions to check if a query has all required variables available before execution - Added
Lotus.SQL.Transformerfor transforming SQL queries to ensure database-specific syntax compatibility when using lotus variables - Added
Lotus.SourceUtilsmodule providing utility functions for detecting data source types and feature support across adapters - Added comprehensive interval query transformation support for PostgreSQL (INTERVAL syntax, make_interval functions)
- Added quoted wildcard pattern transformation for database-specific string concatenation (supports both PostgreSQL || and MySQL CONCAT)
- Added quoted variable placeholder stripping for cleaner parameter binding
- Added
reset_read_only/1callback to adapter behaviour for resetting database sessions back to read-write mode after query execution
Enhanced
- Enhanced query execution to automatically transform SQL statements based on target database adapter before parameter binding
Fixed
- CRITICAL: Fixed database session persistence issue where session-level settings (
PRAGMA query_onlyfor SQLite,SET SESSION TRANSACTION READ ONLYandmax_execution_timefor MySQL) were not being properly restored after query execution, causing connection pool pollution that could break subsequent operations. Lotus now uses a robust snapshot/restore pattern to preserve and restore original session state for each database connection.
[0.5.4] - 2025-08-29
Fixed
- Fixed incomplete
@type opts()specification - added missing:repoand:varsoptions to eliminate Dialyzer type errors
[0.5.3] - 2025-08-29
Enhanced
- Added type-specific SQL parameter placeholders for MySQL adapter supporting date, datetime, time, number, integer, boolean, and json types
- Added type-specific SQL parameter placeholders for PostgreSQL adapter supporting date, datetime, time, number, integer, boolean, and json types
- Added
extract_variables_from_statement/1function to extract unique variable names from SQL statements in order of first occurrence - Added
get_option_source/1function to QueryVariable module to determine if options come from query or static sources
Fixed
- Added proper type casting in parameter placeholders to ensure correct SQL data type handling across database adapters
[0.5.2] - 2025-08-28
Enhanced
- Enhanced
Lotus.run_query/2variable resolution to properly merge default variable values with runtime overrides viavarsoption - Improved
Lotus.run_query/2documentation with comprehensive examples showing variable resolution order, type casting, and usage patterns
Fixed
- Removed non-functional identifier variable substitution (e.g.,
{{table}}for table names) that would cause Ecto adapter crashes - Clarified documentation that variables are only safe for SQL values (WHERE clauses, ORDER BY values), never for identifiers like table or column names
[0.5.1] - 2025-08-27
Fixed
- Fixed error handling for optional Ecto adapters - removed hardcoded references to specific adapter error structs (
Postgrex.Error,MyXQL.Error,Exqlite.Error) to prevent compilation crashes when those adapters are not installed in host applications - Added MySQL preflight authorization support with intelligent alias resolution and schema-qualified table parsing
Changed
- Refactored adapter error formatting to use dynamic error type checking instead of pattern matching on specific error structs
[0.5.0] - 2025-08-26
Added
- Introduced
Lotus.Adapterbehaviour with dedicated implementations for PostgreSQL, SQLite, MySQL, and Default - Added MySQL support with full adapter implementation using
:myxqldependency - Added
default_repoconfiguration option for cleaner multi-database setup - Added
param_placeholder/3callback to adapter behaviour for generating database-specific SQL parameter placeholders - Added
builtin_denies/1callback to allow adapters to define system table filtering rules - Added
handled_errors/0callback to allow adapters to declare which exceptions they format - Added MySQL development environment setup with Docker Compose
Changed
- BREAKING: Removed
param_style/1API in favor ofparam_placeholder/4facade that delegates to adapters - BREAKING: Storage repo no longer used as fallback for query execution - only data repos are valid execution targets
- Enhanced configuration validation to require
default_repowhen multiple data repositories are configured - Refactored error formatting to delegate based on
handled_errors/0, ensuring cleaner and more extensible error handling - Refactored table visibility system to use adapter-specific
builtin_denies/1for system table filtering - Consolidated adapter tests into a single facade-level
Lotus.AdapterTestcovering all supported databases
[0.4.0] - 2025-08-26
Added
- Database-level read-only protection for SQLite using
PRAGMA query_only(SQLite 3.8.0+) - Comprehensive CTE (Common Table Expression) destructive operation tests for both PostgreSQL and SQLite
Changed
- BREAKING: Replaced
var_defaultsfield with structuredvariablesfield for enhanced UI integration - BREAKING: Changed variable placeholder syntax from
{var}to{{var}}for better parsing - BREAKING: Database schema migration removes
var_defaultscolumn and addsvariablescolumn tolotus_queriestable - Enhanced QueryVariable schema with type definitions (text, number, date), widget controls (input, select), labels, and option support
- Added
static_optionsfield for predefined dropdown choices - Added
options_queryfield for dynamic dropdown population from database queries - Added validation to ensure select widgets define either
static_optionsoroptions_query
[0.3.3] - 2025-08-25
Changed
- Improved table visibility rules: bare string patterns (e.g.,
"api_keys") now match table names across all schemas in PostgreSQL, not just nil/empty schemas. This provides a more intuitive API where"api_keys"blocks the table in any schema, while{"public", "api_keys"}blocks it only in the public schema.
[0.3.2] - 2025-08-25
- Change
statementcol from varchar to text
[0.3.1] - 2025-08-25
- Specify
repooption inLotus.run_sql
[0.3.0] - 2025-08-25
- BREAKING: Removed
tagsfield from queries - queries no longer support tagging/filtering by tags - BREAKING: Changed query field from
query(map withsqlandparams) tostatement(string) - Add smart variable support with
{var}placeholders in SQL statements - Add
var_defaultsfield to queries for providing default variable values - Add comprehensive adapter tests for
Lotus.Adaptermodule - Add
get_queryto fetch queries without raising when they don't exist
[0.2.0] - 2025-01-21
- BREAKING: Removed support for fk and pk configuration options
- BREAKING: Changed configuration structure -
repoconfig replaced withecto_repoanddata_repos - BREAKING: Removed unused
prefixoption from query execution opts (was never implemented) - BREAKING:
list_tablesnow returns{schema, table}tuples instead of just table names - Add PostgreSQL per-query
search_pathsupport for multi-schema applications - Add
search_pathfield to stored queries for automatic schema resolution - Add runtime
search_pathoverride option for ad-hoc queries - Add
search_pathvalidation to prevent injection attacks - Add preflight authorization support for
search_path(EXPLAIN uses same path as execution) - Add multi-schema support to
list_tables,get_table_schema, andget_table_stats - Add
list_relationsfunction to return tables with schema information - Add schema-aware table discovery with
:schema,:schemas, and:search_pathoptions - Add multi-database support with PostgreSQL and SQLite
- Add table visibility controls for enhanced security
- Add support for multiple data repositories with flexible routing
- Add
data_repofield to stored queries for automatic repository selection - Add comprehensive development environment setup with sample data
- Add read-only repository configuration guidance
[0.1.0] - 2025-01-09
- Initial release
- Query storage, execution, and basic filtering
- Read-only SQL runner with safety checks