fhir-persistence 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.5.0] - 2025-03-16
9
+
10
+ ### Fixed
11
+
12
+ #### PostgreSQL Migration Path Fixes
13
+
14
+ - **`migration-generator.ts`** — IG migration path now creates PostgreSQL extensions (`pg_trgm`, `btree_gin`) and helper function (`token_array_to_text`) before generating GIN indexes, fixing "no default operator class for access method gin" error
15
+ - **`where-builder.ts`** — Lookup table EXISTS subqueries now use fully-qualified `"ResourceType"."id"` instead of ambiguous `"id"`, fixing PostgreSQL "operator does not exist: text = integer" error in name/address/telecom searches
16
+
17
+ ### Changed
18
+
19
+ - **`buildWhereFragment` (v1)** — Added optional `resourceType` parameter, passed to `buildLookupTableFragment`
20
+ - **`buildWhereFragmentV2` (v2)** — Added optional `resourceType` parameter, passed to `buildLookupTableFragmentV2`
21
+ - Both `buildLookupTableFragment*` functions now generate `outerIdRef = "ResourceType"."id"` to eliminate column name ambiguity in PostgreSQL
22
+
23
+ ### Test Coverage
24
+
25
+ - **1014 total tests** (1006 passing, 8 skipped) across 56 test files — no regressions
26
+ - Updated 7 lookup table test assertions to use qualified column references
27
+
28
+ ## [0.4.0] - 2025-03-15
29
+
30
+ ### Fixed
31
+
32
+ #### PostgreSQL DDL Compatibility (Phase D)
33
+
34
+ - **`migration-runner.ts`** — Tracking table `_migrations` now uses `TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP` on PostgreSQL instead of SQLite-only `datetime('now')`
35
+ - **`package-registry-repo.ts`** — `_packages` and `_schema_version` tables use dialect-aware timestamp defaults; `INSERT OR REPLACE` replaced with `INSERT ... ON CONFLICT ... DO UPDATE` on PostgreSQL
36
+ - **`reindex-scheduler.ts`** — `_reindex_jobs` table uses `SERIAL PRIMARY KEY` on PostgreSQL instead of `AUTOINCREMENT`; `datetime('now')` replaced with `CURRENT_TIMESTAMP`
37
+ - **`valueset-repo.ts`** — `terminology_valuesets` table uses dialect-aware timestamp defaults; `INSERT OR REPLACE` replaced with `INSERT ... ON CONFLICT ... DO UPDATE` on PostgreSQL
38
+ - **`lookup-table-writer.ts`** — 4 global lookup tables (`HumanName`, `Address`, `ContactPoint`, `Identifier`) use `SERIAL PRIMARY KEY` on PostgreSQL instead of `AUTOINCREMENT`
39
+
40
+ ### Changed
41
+
42
+ - **`ig-persistence-manager.ts`** — Now passes `dialect` to `PackageRegistryRepo`, `MigrationRunnerV2`, and `ReindexScheduler` constructors
43
+ - **`indexing-pipeline.ts`** — `IndexingPipelineOptions` accepts `dialect` parameter, passed to `LookupTableWriter`
44
+ - **`fhir-system.ts`** — Passes `dialect` through to `IndexingPipeline` via `FhirPersistence` options
45
+ - All new `dialect` parameters default to `'sqlite'` — fully backward-compatible
46
+
47
+ ### Test Coverage
48
+
49
+ - **1014 total tests** (1006 passing, 8 skipped) across 56 test files — no regressions
50
+
8
51
  ## [0.3.0] - 2025-03-15
9
52
 
10
53
  ### Added
package/README.md CHANGED
@@ -5,7 +5,7 @@ Embedded FHIR R4 persistence layer — CRUD, search, indexing, and schema migrat
5
5
  [![npm version](https://img.shields.io/npm/v/fhir-persistence)](https://www.npmjs.com/package/fhir-persistence)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
7
7
 
8
- > **v0.3.0** — Dual-backend validated: 1014 tests across SQLite and PostgreSQL
8
+ > **v0.5.0** — PostgreSQL migration path complete: GIN extensions + lookup table fixes
9
9
 
10
10
  ## Features
11
11
 
@@ -5202,12 +5202,12 @@ function arrayContainsLikeV2(col, value, dialect) {
5202
5202
  }
5203
5203
  return { sql: `EXISTS (SELECT 1 FROM json_each(${col}) WHERE json_each.value LIKE ?)`, values: [value] };
5204
5204
  }
5205
- function buildWhereFragmentV2(impl, param, dialect) {
5205
+ function buildWhereFragmentV2(impl, param, dialect, resourceType) {
5206
5206
  if (param.modifier === "missing") {
5207
5207
  return buildMissingFragmentV2(impl, param);
5208
5208
  }
5209
5209
  if (impl.strategy === "lookup-table") {
5210
- return buildLookupTableFragmentV2(impl, param);
5210
+ return buildLookupTableFragmentV2(impl, param, resourceType ?? "Resource");
5211
5211
  }
5212
5212
  if (impl.strategy === "token-column") {
5213
5213
  return buildTokenColumnFragmentV2(impl, param, dialect);
@@ -5235,7 +5235,7 @@ function buildMissingFragmentV2(impl, param) {
5235
5235
  const col = quoteColumn(impl.columnName);
5236
5236
  return { sql: isMissing ? `${col} IS NULL` : `${col} IS NOT NULL`, values: [] };
5237
5237
  }
5238
- function buildLookupTableFragmentV2(impl, param) {
5238
+ function buildLookupTableFragmentV2(impl, param, resourceType) {
5239
5239
  const mapping = LOOKUP_TABLE_MAP[impl.code];
5240
5240
  if (!mapping) {
5241
5241
  const sortCol = quoteColumn(`__${impl.columnName}Sort`);
@@ -5245,25 +5245,26 @@ function buildLookupTableFragmentV2(impl, param) {
5245
5245
  }
5246
5246
  const { table, column } = mapping;
5247
5247
  const colRef = `__lookup."${column}"`;
5248
+ const outerIdRef = `"${resourceType}"."id"`;
5248
5249
  if (param.modifier === "exact") {
5249
5250
  if (param.values.length === 1) {
5250
- return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = "id" AND ${colRef} = ?)`, values: [param.values[0]] };
5251
+ return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = ${outerIdRef} AND ${colRef} = ?)`, values: [param.values[0]] };
5251
5252
  }
5252
5253
  const conds2 = param.values.map(() => `${colRef} = ?`);
5253
- return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = "id" AND (${conds2.join(" OR ")}))`, values: [...param.values] };
5254
+ return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = ${outerIdRef} AND (${conds2.join(" OR ")}))`, values: [...param.values] };
5254
5255
  }
5255
5256
  if (param.modifier === "contains") {
5256
5257
  if (param.values.length === 1) {
5257
- return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = "id" AND LOWER(${colRef}) LIKE ?)`, values: [`%${param.values[0].toLowerCase()}%`] };
5258
+ return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = ${outerIdRef} AND LOWER(${colRef}) LIKE ?)`, values: [`%${param.values[0].toLowerCase()}%`] };
5258
5259
  }
5259
5260
  const conds2 = param.values.map(() => `LOWER(${colRef}) LIKE ?`);
5260
- return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = "id" AND (${conds2.join(" OR ")}))`, values: param.values.map((v) => `%${v.toLowerCase()}%`) };
5261
+ return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = ${outerIdRef} AND (${conds2.join(" OR ")}))`, values: param.values.map((v) => `%${v.toLowerCase()}%`) };
5261
5262
  }
5262
5263
  if (param.values.length === 1) {
5263
- return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = "id" AND LOWER(${colRef}) LIKE ?)`, values: [`${param.values[0].toLowerCase()}%`] };
5264
+ return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = ${outerIdRef} AND LOWER(${colRef}) LIKE ?)`, values: [`${param.values[0].toLowerCase()}%`] };
5264
5265
  }
5265
5266
  const conds = param.values.map(() => `LOWER(${colRef}) LIKE ?`);
5266
- return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = "id" AND (${conds.join(" OR ")}))`, values: param.values.map((v) => `${v.toLowerCase()}%`) };
5267
+ return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = ${outerIdRef} AND (${conds.join(" OR ")}))`, values: param.values.map((v) => `${v.toLowerCase()}%`) };
5267
5268
  }
5268
5269
  function buildStringFragmentV2(impl, param) {
5269
5270
  const col = quoteColumn(impl.columnName);
@@ -5432,7 +5433,7 @@ function buildWhereClauseV2(params, registry, resourceType, dialect) {
5432
5433
  }
5433
5434
  const impl = resolveImplV2(param, registry, resourceType);
5434
5435
  if (!impl) continue;
5435
- const fragment = buildWhereFragmentV2(impl, param, dialect);
5436
+ const fragment = buildWhereFragmentV2(impl, param, dialect, resourceType);
5436
5437
  if (fragment) {
5437
5438
  fragments.push(fragment);
5438
5439
  }
@@ -6560,16 +6561,18 @@ var FhirStore = class {
6560
6561
  var import_node_crypto5 = require("node:crypto");
6561
6562
 
6562
6563
  // src/repo/lookup-table-writer.ts
6563
- var LOOKUP_TABLE_DDL = {
6564
- HumanName: `CREATE TABLE IF NOT EXISTS "HumanName" (
6565
- "id" INTEGER PRIMARY KEY AUTOINCREMENT,
6564
+ function buildLookupTableDDL(dialect) {
6565
+ const pk = dialect === "postgres" ? '"id" SERIAL PRIMARY KEY' : '"id" INTEGER PRIMARY KEY AUTOINCREMENT';
6566
+ return {
6567
+ HumanName: `CREATE TABLE IF NOT EXISTS "HumanName" (
6568
+ ${pk},
6566
6569
  "resourceId" TEXT NOT NULL,
6567
6570
  "name" TEXT,
6568
6571
  "given" TEXT,
6569
6572
  "family" TEXT
6570
6573
  )`,
6571
- Address: `CREATE TABLE IF NOT EXISTS "Address" (
6572
- "id" INTEGER PRIMARY KEY AUTOINCREMENT,
6574
+ Address: `CREATE TABLE IF NOT EXISTS "Address" (
6575
+ ${pk},
6573
6576
  "resourceId" TEXT NOT NULL,
6574
6577
  "address" TEXT,
6575
6578
  "city" TEXT,
@@ -6578,20 +6581,21 @@ var LOOKUP_TABLE_DDL = {
6578
6581
  "state" TEXT,
6579
6582
  "use" TEXT
6580
6583
  )`,
6581
- ContactPoint: `CREATE TABLE IF NOT EXISTS "ContactPoint" (
6582
- "id" INTEGER PRIMARY KEY AUTOINCREMENT,
6584
+ ContactPoint: `CREATE TABLE IF NOT EXISTS "ContactPoint" (
6585
+ ${pk},
6583
6586
  "resourceId" TEXT NOT NULL,
6584
6587
  "system" TEXT,
6585
6588
  "value" TEXT,
6586
6589
  "use" TEXT
6587
6590
  )`,
6588
- Identifier: `CREATE TABLE IF NOT EXISTS "Identifier" (
6589
- "id" INTEGER PRIMARY KEY AUTOINCREMENT,
6591
+ Identifier: `CREATE TABLE IF NOT EXISTS "Identifier" (
6592
+ ${pk},
6590
6593
  "resourceId" TEXT NOT NULL,
6591
6594
  "system" TEXT,
6592
6595
  "value" TEXT
6593
6596
  )`
6594
- };
6597
+ };
6598
+ }
6595
6599
  var LOOKUP_TABLE_INDEXES = {
6596
6600
  HumanName: [
6597
6601
  'CREATE INDEX IF NOT EXISTS "HumanName_resourceId_idx" ON "HumanName" ("resourceId")',
@@ -6617,10 +6621,12 @@ var LOOKUP_COLUMNS = {
6617
6621
  Identifier: ["resourceId", "system", "value"]
6618
6622
  };
6619
6623
  var LookupTableWriter = class {
6620
- constructor(adapter) {
6624
+ constructor(adapter, dialect = "sqlite") {
6621
6625
  this.adapter = adapter;
6626
+ this.ddl = buildLookupTableDDL(dialect);
6622
6627
  }
6623
6628
  initialized = false;
6629
+ ddl;
6624
6630
  // ---------------------------------------------------------------------------
6625
6631
  // DDL
6626
6632
  // ---------------------------------------------------------------------------
@@ -6629,8 +6635,8 @@ var LookupTableWriter = class {
6629
6635
  */
6630
6636
  async ensureTables() {
6631
6637
  if (this.initialized) return;
6632
- for (const table of Object.keys(LOOKUP_TABLE_DDL)) {
6633
- await this.adapter.execute(LOOKUP_TABLE_DDL[table]);
6638
+ for (const table of Object.keys(this.ddl)) {
6639
+ await this.adapter.execute(this.ddl[table]);
6634
6640
  for (const idx of LOOKUP_TABLE_INDEXES[table]) {
6635
6641
  await this.adapter.execute(idx);
6636
6642
  }
@@ -6679,7 +6685,7 @@ var LookupTableWriter = class {
6679
6685
  */
6680
6686
  async deleteRows(resourceId) {
6681
6687
  await this.ensureTables();
6682
- for (const table of Object.keys(LOOKUP_TABLE_DDL)) {
6688
+ for (const table of Object.keys(this.ddl)) {
6683
6689
  await this.adapter.execute(
6684
6690
  `DELETE FROM "${table}" WHERE "resourceId" = ?`,
6685
6691
  [resourceId]
@@ -6705,7 +6711,7 @@ var LookupTableWriter = class {
6705
6711
  var IndexingPipeline = class {
6706
6712
  constructor(adapter, options) {
6707
6713
  this.adapter = adapter;
6708
- this.lookupWriter = new LookupTableWriter(adapter);
6714
+ this.lookupWriter = new LookupTableWriter(adapter, options?.dialect ?? "sqlite");
6709
6715
  this.runtimeProvider = options?.runtimeProvider;
6710
6716
  this.options = {
6711
6717
  enableLookupTables: options?.enableLookupTables ?? true,
@@ -7275,6 +7281,14 @@ function generateMigration(deltas, dialect) {
7275
7281
  const down = [];
7276
7282
  const reindexDeltas = [];
7277
7283
  const descriptions = [];
7284
+ const needsPgExtensions = deltas.some((d) => d.kind === "ADD_TABLE" || d.kind === "ADD_INDEX");
7285
+ if (dialect === "postgres" && needsPgExtensions) {
7286
+ up.push("CREATE EXTENSION IF NOT EXISTS pg_trgm;");
7287
+ up.push("CREATE EXTENSION IF NOT EXISTS btree_gin;");
7288
+ up.push(
7289
+ `CREATE OR REPLACE FUNCTION token_array_to_text(arr text[]) RETURNS text LANGUAGE sql IMMUTABLE AS $$ SELECT array_to_string(arr, ' ') $$;`
7290
+ );
7291
+ }
7278
7292
  for (const delta of deltas) {
7279
7293
  switch (delta.kind) {
7280
7294
  case "ADD_TABLE": {
@@ -7403,35 +7417,43 @@ function mapColumnTypeForDialect(type, dialect) {
7403
7417
 
7404
7418
  // src/registry/package-registry-repo.ts
7405
7419
  var PACKAGES_TABLE = "_packages";
7406
- var CREATE_PACKAGES_TABLE = `
7420
+ var SCHEMA_VERSION_TABLE = "_schema_version";
7421
+ function createPackagesTableDDL(dialect) {
7422
+ const ts = dialect === "postgres" ? "TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP" : "TEXT NOT NULL DEFAULT (datetime('now'))";
7423
+ return `
7407
7424
  CREATE TABLE IF NOT EXISTS "${PACKAGES_TABLE}" (
7408
7425
  "name" TEXT NOT NULL,
7409
7426
  "version" TEXT NOT NULL,
7410
7427
  "checksum" TEXT NOT NULL,
7411
7428
  "schemaSnapshot" TEXT,
7412
- "installedAt" TEXT NOT NULL DEFAULT (datetime('now')),
7429
+ "installedAt" ${ts},
7413
7430
  "status" TEXT NOT NULL DEFAULT 'active',
7414
7431
  PRIMARY KEY ("name", "version")
7415
7432
  );
7416
7433
  `;
7417
- var SCHEMA_VERSION_TABLE = "_schema_version";
7418
- var CREATE_SCHEMA_VERSION_TABLE = `
7434
+ }
7435
+ function createSchemaVersionTableDDL(dialect) {
7436
+ const ts = dialect === "postgres" ? "TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP" : "TEXT NOT NULL DEFAULT (datetime('now'))";
7437
+ return `
7419
7438
  CREATE TABLE IF NOT EXISTS "${SCHEMA_VERSION_TABLE}" (
7420
7439
  "version" INTEGER NOT NULL PRIMARY KEY,
7421
7440
  "packageList" TEXT NOT NULL,
7422
7441
  "description" TEXT NOT NULL DEFAULT '',
7423
- "appliedAt" TEXT NOT NULL DEFAULT (datetime('now'))
7442
+ "appliedAt" ${ts}
7424
7443
  );
7425
7444
  `;
7445
+ }
7426
7446
  var PackageRegistryRepo = class {
7427
- constructor(adapter) {
7447
+ constructor(adapter, dialect = "sqlite") {
7428
7448
  this.adapter = adapter;
7449
+ this.dialect = dialect;
7429
7450
  }
7451
+ dialect;
7430
7452
  /**
7431
7453
  * Ensure the packages tracking table exists.
7432
7454
  */
7433
7455
  async ensureTable() {
7434
- await this.adapter.execute(CREATE_PACKAGES_TABLE);
7456
+ await this.adapter.execute(createPackagesTableDDL(this.dialect));
7435
7457
  }
7436
7458
  /**
7437
7459
  * Get the active version of a package by name.
@@ -7476,7 +7498,7 @@ var PackageRegistryRepo = class {
7476
7498
  [pkg.name]
7477
7499
  );
7478
7500
  await this.adapter.execute(
7479
- `INSERT OR REPLACE INTO "${PACKAGES_TABLE}" ("name", "version", "checksum", "schemaSnapshot", "status") VALUES (?, ?, ?, ?, 'active')`,
7501
+ this.upsertSQL(),
7480
7502
  [pkg.name, pkg.version, pkg.checksum, pkg.schemaSnapshot ?? null]
7481
7503
  );
7482
7504
  await this.recordSchemaVersion(description ?? `Register ${pkg.name}@${pkg.version}`);
@@ -7493,10 +7515,19 @@ var PackageRegistryRepo = class {
7493
7515
  [pkg.name]
7494
7516
  );
7495
7517
  await this.adapter.execute(
7496
- `INSERT OR REPLACE INTO "${PACKAGES_TABLE}" ("name", "version", "checksum", "schemaSnapshot", "status") VALUES (?, ?, ?, ?, 'active')`,
7518
+ this.upsertSQL(),
7497
7519
  [pkg.name, pkg.version, pkg.checksum, pkg.schemaSnapshot ?? null]
7498
7520
  );
7499
7521
  }
7522
+ /**
7523
+ * Generate dialect-aware UPSERT SQL.
7524
+ */
7525
+ upsertSQL() {
7526
+ if (this.dialect === "postgres") {
7527
+ return `INSERT INTO "${PACKAGES_TABLE}" ("name", "version", "checksum", "schemaSnapshot", "status") VALUES (?, ?, ?, ?, 'active') ON CONFLICT ("name", "version") DO UPDATE SET "checksum" = EXCLUDED."checksum", "schemaSnapshot" = EXCLUDED."schemaSnapshot", "status" = 'active'`;
7528
+ }
7529
+ return `INSERT OR REPLACE INTO "${PACKAGES_TABLE}" ("name", "version", "checksum", "schemaSnapshot", "status") VALUES (?, ?, ?, ?, 'active')`;
7530
+ }
7500
7531
  /**
7501
7532
  * Remove all versions of a package.
7502
7533
  */
@@ -7528,7 +7559,7 @@ var PackageRegistryRepo = class {
7528
7559
  * Ensure the schema version table exists.
7529
7560
  */
7530
7561
  async ensureSchemaVersionTable() {
7531
- await this.adapter.execute(CREATE_SCHEMA_VERSION_TABLE);
7562
+ await this.adapter.execute(createSchemaVersionTableDDL(this.dialect));
7532
7563
  }
7533
7564
  /**
7534
7565
  * Record a schema version with the current active package list.
@@ -7578,12 +7609,25 @@ CREATE TABLE IF NOT EXISTS "${TRACKING_TABLE_V2}" (
7578
7609
  "applied_at" TEXT NOT NULL DEFAULT (datetime('now'))
7579
7610
  );
7580
7611
  `;
7612
+ var CREATE_TRACKING_TABLE_V2_POSTGRES = `
7613
+ CREATE TABLE IF NOT EXISTS "${TRACKING_TABLE_V2}" (
7614
+ "version" INTEGER PRIMARY KEY,
7615
+ "description" TEXT NOT NULL,
7616
+ "type" TEXT NOT NULL DEFAULT 'file',
7617
+ "applied_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
7618
+ );
7619
+ `;
7620
+ function createTrackingDDL(dialect) {
7621
+ return dialect === "postgres" ? CREATE_TRACKING_TABLE_V2_POSTGRES : CREATE_TRACKING_TABLE_V2_SQLITE;
7622
+ }
7581
7623
  var MigrationRunnerV2 = class {
7582
7624
  adapter;
7583
7625
  migrations;
7584
- constructor(adapter, migrations = []) {
7626
+ dialect;
7627
+ constructor(adapter, migrations = [], dialect = "sqlite") {
7585
7628
  this.adapter = adapter;
7586
7629
  this.migrations = [...migrations].sort((a, b) => a.version - b.version);
7630
+ this.dialect = dialect;
7587
7631
  }
7588
7632
  // ---------------------------------------------------------------------------
7589
7633
  // Public API
@@ -7592,7 +7636,7 @@ var MigrationRunnerV2 = class {
7592
7636
  * Ensure the tracking table exists.
7593
7637
  */
7594
7638
  async ensureTrackingTable() {
7595
- await this.adapter.execute(CREATE_TRACKING_TABLE_V2_SQLITE);
7639
+ await this.adapter.execute(createTrackingDDL(this.dialect));
7596
7640
  }
7597
7641
  /**
7598
7642
  * Apply all pending migrations (or up to a target version).
@@ -7768,7 +7812,23 @@ var MigrationRunnerV2 = class {
7768
7812
 
7769
7813
  // src/migration/reindex-scheduler.ts
7770
7814
  var REINDEX_JOBS_TABLE = "_reindex_jobs";
7771
- var CREATE_REINDEX_JOBS_TABLE = `
7815
+ function createReindexJobsTableDDL(dialect) {
7816
+ if (dialect === "postgres") {
7817
+ return `
7818
+ CREATE TABLE IF NOT EXISTS "${REINDEX_JOBS_TABLE}" (
7819
+ "id" SERIAL PRIMARY KEY,
7820
+ "resourceType" TEXT NOT NULL,
7821
+ "searchParamCode" TEXT NOT NULL,
7822
+ "expression" TEXT NOT NULL,
7823
+ "status" TEXT NOT NULL DEFAULT 'pending',
7824
+ "cursor" TEXT,
7825
+ "processedCount" INTEGER NOT NULL DEFAULT 0,
7826
+ "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
7827
+ "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
7828
+ );
7829
+ `;
7830
+ }
7831
+ return `
7772
7832
  CREATE TABLE IF NOT EXISTS "${REINDEX_JOBS_TABLE}" (
7773
7833
  "id" INTEGER PRIMARY KEY AUTOINCREMENT,
7774
7834
  "resourceType" TEXT NOT NULL,
@@ -7781,15 +7841,21 @@ CREATE TABLE IF NOT EXISTS "${REINDEX_JOBS_TABLE}" (
7781
7841
  "updatedAt" TEXT NOT NULL DEFAULT (datetime('now'))
7782
7842
  );
7783
7843
  `;
7844
+ }
7845
+ function nowExpression(dialect) {
7846
+ return dialect === "postgres" ? "CURRENT_TIMESTAMP" : "datetime('now')";
7847
+ }
7784
7848
  var ReindexScheduler = class {
7785
- constructor(adapter) {
7849
+ constructor(adapter, dialect = "sqlite") {
7786
7850
  this.adapter = adapter;
7851
+ this.dialect = dialect;
7787
7852
  }
7853
+ dialect;
7788
7854
  /**
7789
7855
  * Ensure the reindex jobs table exists.
7790
7856
  */
7791
7857
  async ensureTable() {
7792
- await this.adapter.execute(CREATE_REINDEX_JOBS_TABLE);
7858
+ await this.adapter.execute(createReindexJobsTableDDL(this.dialect));
7793
7859
  }
7794
7860
  /**
7795
7861
  * Schedule reindex jobs from REINDEX deltas.
@@ -7856,7 +7922,7 @@ var ReindexScheduler = class {
7856
7922
  sets.push('"processedCount" = ?');
7857
7923
  values.push(update.processedCount);
7858
7924
  }
7859
- sets.push(`"updatedAt" = datetime('now')`);
7925
+ sets.push(`"updatedAt" = ${nowExpression(this.dialect)}`);
7860
7926
  values.push(id);
7861
7927
  await this.adapter.execute(
7862
7928
  `UPDATE "${REINDEX_JOBS_TABLE}" SET ${sets.join(", ")} WHERE "id" = ?`,
@@ -7889,9 +7955,9 @@ var IGPersistenceManager = class {
7889
7955
  reindexScheduler;
7890
7956
  constructor(adapter, dialect = "sqlite") {
7891
7957
  this.dialect = dialect;
7892
- this.packageRepo = new PackageRegistryRepo(adapter);
7893
- this.migrationRunner = new MigrationRunnerV2(adapter);
7894
- this.reindexScheduler = new ReindexScheduler(adapter);
7958
+ this.packageRepo = new PackageRegistryRepo(adapter, dialect);
7959
+ this.migrationRunner = new MigrationRunnerV2(adapter, [], dialect);
7960
+ this.reindexScheduler = new ReindexScheduler(adapter, dialect);
7895
7961
  }
7896
7962
  /**
7897
7963
  * Initialize an IG package — the main entry point.
@@ -8072,25 +8138,30 @@ var TerminologyCodeRepo = class {
8072
8138
 
8073
8139
  // src/terminology/valueset-repo.ts
8074
8140
  var VALUESETS_TABLE = "terminology_valuesets";
8075
- var CREATE_VALUESETS_TABLE = `
8141
+ function createValuesetsTableDDL(dialect) {
8142
+ const ts = dialect === "postgres" ? "TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP" : "TEXT NOT NULL DEFAULT (datetime('now'))";
8143
+ return `
8076
8144
  CREATE TABLE IF NOT EXISTS "${VALUESETS_TABLE}" (
8077
8145
  "url" TEXT NOT NULL,
8078
8146
  "version" TEXT NOT NULL,
8079
8147
  "name" TEXT,
8080
8148
  "content" TEXT NOT NULL,
8081
- "storedAt" TEXT NOT NULL DEFAULT (datetime('now')),
8149
+ "storedAt" ${ts},
8082
8150
  PRIMARY KEY ("url", "version")
8083
8151
  );
8084
8152
  `;
8153
+ }
8085
8154
  var ValueSetRepo = class {
8086
- constructor(adapter) {
8155
+ constructor(adapter, dialect = "sqlite") {
8087
8156
  this.adapter = adapter;
8157
+ this.dialect = dialect;
8088
8158
  }
8159
+ dialect;
8089
8160
  /**
8090
8161
  * Ensure the terminology_valuesets table exists.
8091
8162
  */
8092
8163
  async ensureTable() {
8093
- await this.adapter.execute(CREATE_VALUESETS_TABLE);
8164
+ await this.adapter.execute(createValuesetsTableDDL(this.dialect));
8094
8165
  }
8095
8166
  /**
8096
8167
  * Insert or update a ValueSet.
@@ -8098,10 +8169,8 @@ var ValueSetRepo = class {
8098
8169
  */
8099
8170
  async upsert(input) {
8100
8171
  await this.ensureTable();
8101
- await this.adapter.execute(
8102
- `INSERT OR REPLACE INTO "${VALUESETS_TABLE}" ("url", "version", "name", "content") VALUES (?, ?, ?, ?)`,
8103
- [input.url, input.version, input.name ?? null, input.content]
8104
- );
8172
+ const sql = this.dialect === "postgres" ? `INSERT INTO "${VALUESETS_TABLE}" ("url", "version", "name", "content") VALUES (?, ?, ?, ?) ON CONFLICT ("url", "version") DO UPDATE SET "name" = EXCLUDED."name", "content" = EXCLUDED."content"` : `INSERT OR REPLACE INTO "${VALUESETS_TABLE}" ("url", "version", "name", "content") VALUES (?, ?, ?, ?)`;
8173
+ await this.adapter.execute(sql, [input.url, input.version, input.name ?? null, input.content]);
8105
8174
  }
8106
8175
  /**
8107
8176
  * Get a specific ValueSet by url and version.
@@ -8560,7 +8629,8 @@ var FhirSystem = class {
8560
8629
  indexing: {
8561
8630
  enableLookupTables: this.options.enableLookupTables,
8562
8631
  enableReferences: this.options.enableReferences,
8563
- runtimeProvider: this.options.runtimeProvider
8632
+ runtimeProvider: this.options.runtimeProvider,
8633
+ dialect: this.dialect
8564
8634
  }
8565
8635
  });
8566
8636
  return {