Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ jobs:
shell: pwsh
run: |
Install-Module PSScriptAnalyzer -Scope CurrentUser -Force -RequiredVersion 1.25.0
$findings = foreach ($file in 'PSLocalBase.psm1', 'InitWinSqlite3Native.ps1', 'PSLocalBase.psd1') {
. ./InitWinSqlite3Native.ps1
$findings = foreach ($file in 'PSLocalBase.psm1', 'PSLocalBase.Cmdlets.ps1', 'InitWinSqlite3Native.ps1', 'PSLocalBase.psd1') {
Invoke-ScriptAnalyzer -Path "./$file" -Severity Error, Warning | Where-Object Severity -ne 'Information'
}
$findings | Format-Table ScriptName, Line, Severity, RuleName, Message -AutoSize -Wrap
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ scratch/
# IDE and Editor configurations
.vscode/
.idea/
.codegraph/

# Windows OS metadata
Thumbs.db
Expand Down
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,46 @@

All notable changes to PSLocalBase are documented here. The project follows [Semantic Versioning](https://semver.org/).

## [2.0.3] - 2026-11-04

The numeric guard requires native bridge version 5. Restart PowerShell after upgrading an already loaded module.

### Fixed
- Reject an empty `Remove-LocalBaseDocument -Filter` without a document ID before deleting any data; direct intentional collection-wide deletion to `Clear-LocalBaseCollection`.
- Apply logical AND when `Remove-LocalBaseDocument` receives both a document ID (explicitly or through pipeline identity) and a filter.
- Resolve relative `Backup-LocalBaseDatabase` destinations against the current PowerShell location before executing the backup.
- Exclude matching SQLite auxiliary tables without `key` and `doc` columns from `collectionGroup()` queries.
- Normalize `not-like` operator syntax in `where()` to accept hyphen, underscore, and space (`'not-like'`, `'not_like'`, `'not like'`, `'-notlike'`) for full syntax symmetry with `not-in`.
- Guard numeric SQL extraction against unsupported stored values as well as parameters; reject ambiguous comparisons atomically, including membership/deletion with legacy indexes. Preserve Int64 binding and document conservative binary64 limits and index-performance tradeoffs.
- Open/validate replacement ambient connections before closing the original; failed paths/options preserve pending transactions.
- Preserve top-level arrays, scalars and null across Get/Set/Add/Remove with trusted `.Value` envelopes; prevent ConvertFrom-Json array enumeration and explicitly serialize null on PS5.1.
- Reject distinct combined with limit/skip/orderBy instead of ignoring scope.
- Correct materialization/streaming claims and recommend bounded paging.
- Include Cmdlets in CI static analysis; implement ShouldProcess on New collection/index, Set and Start transaction without warning suppressions.
- Pipeline Set/Update/Remove now prioritize explicit arguments, then Get's object identity, then content fallback. Collection/id collisions cannot redirect mutations; adapter decorations are not persisted and colliding payload metadata is preserved.
- `doc()` and `where()` preserve each other's constraints. Reads and deletion combine key and filters with AND; filtered set/update/upsert now throw instead of ignoring filters.
- Transaction blocks use the selected database as their scoped ambient connection and treat ordinary non-terminating errors as failures. Context and error preference are restored on success/failure; same-connection nesting is rejected before entering the inner block.
- `Add-LocalBaseDocument -Id` inserts and rejects duplicates instead of overwriting them.
- Serialization enforces the same UTF-16 byte and JSON nesting limits as reads, preserving previous data on failed targeted or bulk writes.
- `Connect-LocalBase -JournalMode` accepts only the implemented WAL/DELETE modes; unsupported modes are rejected before changing the connection.
- Database removal reports filesystem failures, rejects active files and directories, and requires explicit disconnection. Force permits read-only removal while respecting WhatIf/Confirm.

### Tests
- Add regression coverage for empty-filter rejection with document preservation, conditional ID-and-filter deletion, relative backup paths and collection groups containing auxiliary tables.
- Final review of `e93d1ac`: all 22 suites passed on both PowerShell 7.6.9 and Windows PowerShell 5.1; Playground verification passed on both editions, and PSScriptAnalyzer reported no errors or warnings.
- Added second-review regressions for numeric collisions and atomic failures, reconnects, all top-level JSON shapes, distinct scope and WhatIf.
- Added the contracts regression suite to the runner for identity collisions, query intersections, scoped transactions, duplicate inserts, write limits, real journal readback, and safe temporary-file removal.

### Documentation
- Correct service examples by serializing enum properties as strings and passing explicit collection/storage keys when updating fluent query results through cmdlets.
- Make the service-audit example clear its previous snapshot before ingesting another, and use a valid command to list module aliases.
- Synchronize current references to 22 functions, 7 aliases, 22 test suites and native bridge version 5; label the earlier audit scorecard as historical.

### Added
- **Native PowerShell Cmdlets & Pipeline Adapter (`PSLocalBase.Cmdlets.ps1`)**: Added 22 native cmdlets and 7 short interactive aliases (`gld`, `ald`, `sld`, `uld`, `rld`, `Open-LocalBase`, `Close-LocalBase`) providing pipeline integration (`|`), ambient session management (`Connect-LocalBase` / `Disconnect-LocalBase`), and `-WhatIf` / `-Confirm` safety over the native engine.
- **Dedicated Cmdlets Test Suite (`_TESTS/PSLocalBase.Cmdlets.Tests.ps1`)**: 18 comprehensive test cases (C01–C18) verifying connection lifecycles, pipeline streaming, deep recursive merging, array mutations, atomic transactions, multi-database routing, and cross-edition parity.
- **Comprehensive Documentation (`_DOCS/CMDLETS.md`)**: Complete reference guide covering pipeline architecture, property binding, safety mechanics, cmdlet syntax, parity matrices, and operational automation scripts.

## [2.0.2] - 2026-10-31

### Packaging & Distribution
Expand Down
67 changes: 66 additions & 1 deletion InitWinSqlite3Native.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public sealed class PSLocalBaseConnectionHandle : SafeHandle {
}

public class WinSqlite3 {
public const int BridgeVersion = 4;
public const int BridgeVersion = 5;
public const int SQLITE_OK = 0;
public const int SQLITE_BUSY = 5;
public const int SQLITE_ROW = 100;
Expand All @@ -49,6 +49,7 @@ public class WinSqlite3 {
private delegate void AuxDestructor(IntPtr data);

// Static roots keep callbacks alive while any connection/statement uses them.
private static readonly ScalarFunction NumericCallback = EvaluateNumeric;
private static readonly ScalarFunction PsLikeCallback = EvaluatePsLike;
private static readonly AuxDestructor PatternDestructor = ReleasePattern;

Expand Down Expand Up @@ -76,9 +77,73 @@ public class WinSqlite3 {

public static int RegisterPsLike(IntPtr db) {
// DIRECTONLY keeps triggers and views stored in an untrusted database file from invoking the callback.
int rc = CreateFunction(db, "pslocalbase_numeric", 1, SQLITE_UTF16 | SQLITE_DIRECTONLY, IntPtr.Zero, NumericCallback, IntPtr.Zero, IntPtr.Zero);
if (rc != SQLITE_OK) return rc;
return CreateFunction(db, "pslocalbase_like", 2, SQLITE_UTF16 | SQLITE_DIRECTONLY, IntPtr.Zero, PsLikeCallback, IntPtr.Zero, IntPtr.Zero);
}

[DllImport("winsqlite3.dll", EntryPoint = "sqlite3_result_text16", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
private static extern void ResultText(IntPtr context, string text, int bytes, IntPtr destructor);

// Conservative comparison domain: Int64 integers; finite binary64 reals with
// a lossless decimal roundtrip through binary64. Strings are skipped.
public static void ValidateNumericJson(string json) {
if (json == null) return;
for (int i = 0; i < json.Length; i++) {
if (json[i] == '"') {
for (++i; i < json.Length; i++) {
if (json[i] == '\\') i++;
else if (json[i] == '"') break;
}
} else if (json[i] == '-' || char.IsDigit(json[i])) {
int start = i;
while (i < json.Length && (char.IsDigit(json[i]) || "-+.eE".IndexOf(json[i]) >= 0)) i++;
string token = json.Substring(start, i - start);
i--;
long integer;
if (token.IndexOfAny(new char[] { '.', 'e', 'E' }) < 0) {
if (!long.TryParse(token, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out integer))
throw new ArgumentException("Numeric precision unsupported: JSON integer exceeds Int64.");
} else {
string mantissa = token.Split('e', 'E')[0].Replace("-", "").Replace(".", "").TrimStart('0').TrimEnd('0');
double real;
if (!double.TryParse(token, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out real)
|| double.IsInfinity(real) || double.IsNaN(real) || Math.Abs(real) >= 9007199254740992d || (real == 0 && mantissa.Length != 0)
|| NormalizeNumber(token) != NormalizeNumber(real.ToString("R", System.Globalization.CultureInfo.InvariantCulture)))
throw new ArgumentException("Numeric precision unsupported: real requires finite binary64, <2^53, and lossless decimal roundtrip.");
}
}
}
}

private static string NormalizeNumber(string token) {
string[] parts = token.ToLowerInvariant().Split('e');
long exponent = parts.Length == 2 ? long.Parse(parts[1], System.Globalization.CultureInfo.InvariantCulture) : 0;
string digits = parts[0];
bool negative = digits.StartsWith("-");
digits = digits.TrimStart('-', '+');
int point = digits.IndexOf('.');
if (point >= 0) { exponent -= digits.Length - point - 1; digits = digits.Replace(".", ""); }
digits = digits.TrimStart('0');
if (digits.Length == 0) return "0";
int length = digits.Length;
digits = digits.TrimEnd('0');
exponent += length - digits.Length;
return (negative ? "-" : "") + digits + "e" + exponent.ToString(System.Globalization.CultureInfo.InvariantCulture);
}

private static void EvaluateNumeric(IntPtr context, int argc, IntPtr argv) {
try {
IntPtr value = Marshal.ReadIntPtr(argv);
if (ValueType(value) == SQLITE_NULL) { ResultNull(context); return; }
string json = ReadValue(value);
ValidateNumericJson(json);
ResultText(context, json, checked(json.Length * 2), SQLITE_TRANSIENT);
} catch (Exception) {
ResultError(context, "Numeric precision unsupported in stored JSON; operation rejected.", -1);
}
}

// Returns the deepest object/array nesting level, ignoring brackets inside double-quoted strings.
// Input that this scanner cannot measure reliably (JSON5 quotes or comments, unbalanced
// brackets, an unterminated string) reports int.MaxValue so callers reject it.
Expand Down
Loading
Loading