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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.19.1] - 2026-09-16

### Fixed
- The sync-owned DDL directory resolves to `src/sql/` again after the database class moved into `src/db/`, so the service no longer dies at boot with ENOENT from `verifySyncTables`.

## [0.19.0] - 2026-09-16

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# XChain Sync

<p align="center">
<img src="https://img.shields.io/badge/version-0.19.0-blue" alt="Version">
<img src="https://img.shields.io/badge/version-0.19.1-blue" alt="Version">
<img src="https://img.shields.io/badge/tests-2%2C779%2B%20passing-brightgreen" alt="Tests">
<img src="https://img.shields.io/badge/node-%3E%3D22-green" alt="Node">
<img src="https://img.shields.io/badge/license-AGPL--3.0--or--later-blue" alt="License">
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "xchain-sync",
"description": "Database replication service for the XChain Platform: syncs indexer and decoder databases to validators and consumers via REST snapshots and WebSocket streaming",
"version": "0.19.0",
"version": "0.19.1",
"license": "AGPL-3.0-or-later",
"repository": {
"type": "git",
Expand Down
14 changes: 12 additions & 2 deletions src/db/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ const poolSizing = require('./pool_sizing');
const stakeWeightCollation = require('../stake_weight_collation_activation');
const utf8mb4Columns = require('../schema/utf8mb4_columns');
const lifecycle = require('../table_lifecycle');

// Resolve the sync-owned DDL directory, src/sql/, one level ABOVE this module.
// Every reader goes through this helper so the boot path and the unit test
// that proves the directory exists on disk cannot resolve different paths.
function sqlDir(){
return path.join(__dirname, '..', 'sql');
}
const { assertValidIdentifier, requireStakeWeight } = require('./shared.js');
const util = require('node:util');
const { getLogger } = require('../observability');
Expand Down Expand Up @@ -223,7 +230,7 @@ class Database {
// ClientSync checks and records halts for decoder replicas too, and
// without the table every decoder client start logged a 1146 probe error.
async verifySyncTables(){
let dir = path.join(__dirname, 'sql');
let dir = sqlDir();
let files = fs.readdirSync(dir);
let db = await this.getConnection();
// One summary line instead of a per-table pair; the error path below still
Expand Down Expand Up @@ -256,7 +263,7 @@ class Database {
// Only for sync-service-owned tables such as sync_meta; replicated tables come
// from the source's own DDL.
async createTableFromFile(file){
let dir = path.join(__dirname, 'sql');
let dir = sqlDir();
let data = fs.readFileSync(dir + '/' + file, "utf8");
let queries = splitSqlStatements(data);
for(let query of queries){
Expand Down Expand Up @@ -1378,4 +1385,7 @@ for(const file of MIXIN_FILES){
// guard is consensus-relevant, so it is tested directly, not only through a query.
Database.requireStakeWeight = requireStakeWeight;

// Exposed so the unit suite resolves the SAME directory the boot path reads.
Database.sqlDir = sqlDir;

module.exports = Database;
82 changes: 82 additions & 0 deletions test/unit/db.test/10_database_sql_dir_resolves_on_disk.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright © 2025–2026 Dankest, LLC
// SPDX-License-Identifier: AGPL-3.0-or-later

'use strict';

const path = require('path');
const {
assert,
sinon,
fs,
makeDb,
fakeConn,
silenceConsole,
} = require('./support/helpers');
const Database = require('../../../src/db');

// Touch the REAL src/sql/ directory with fs left unstubbed: the sibling tests
// stub readdirSync, so only these red when the resolved path drifts off disk.

// List the sync-owned DDL files by name, never from a glob, so a missing file
// fails instead of silently shrinking the expected set.
const SYNC_OWNED_DDL = [
'escrow_leaf_journal.sql',
'merkle_epochs.sql',
'merkle_reorgs.sql',
'state_tree_nodes.sql',
'state_tree_roots.sql',
'sync_halt.sql',
'sync_meta.sql',
];

describe('Database.sqlDir(): the sync-owned DDL directory exists on disk', function () {

it('resolves src/sql/ relative to the repo, not src/db/sql/', function () {
let dir = Database.sqlDir();
let expected = path.resolve(__dirname, '..', '..', '..', 'src', 'sql');
assert.strictEqual(path.resolve(dir), expected);
});

it('the resolved directory exists and is a directory', function () {
let dir = Database.sqlDir();
assert.ok(fs.existsSync(dir), 'missing on disk: ' + dir);
assert.ok(fs.statSync(dir).isDirectory(), 'not a directory: ' + dir);
});

it('contains every sync-owned .sql file the boot path expects', function () {
let dir = Database.sqlDir();
let present = fs.readdirSync(dir).filter(f => f.endsWith('.sql')).sort();
for (const file of SYNC_OWNED_DDL)
assert.ok(present.includes(file), 'missing DDL file: ' + path.join(dir, file));
assert.deepStrictEqual(present, SYNC_OWNED_DDL.slice().sort(),
'the on-disk set differs from the expected sync-owned set');
});
});

describe('Database.verifySyncTables() reads the real DDL directory', function () {
let db;
beforeEach(function () { silenceConsole(); db = makeDb('indexer'); });
afterEach(async function () { sinon.restore(); await db.close(); });

it('probes one table per on-disk DDL file with fs left unstubbed', async function () {
// Only the connection is faked; readdirSync hits the filesystem, which
// is the call that threw ENOENT in production.
let conn = fakeConn([{ TABLE_NAME: 'present' }]);
sinon.stub(db, 'getConnection').resolves(conn);
let result = await db.verifySyncTables();
assert.strictEqual(result, true);
assert.strictEqual(conn.query.callCount, SYNC_OWNED_DDL.length,
'one information_schema probe per sync-owned DDL file');
let probed = conn.query.getCalls().map(c => c.args[1][1]).sort();
assert.deepStrictEqual(probed, SYNC_OWNED_DDL.map(f => f.replace(/\.sql$/, '')).sort());
});

it('createTableFromFile() reads a real DDL file and runs its statements', async function () {
let doQueryStub = sinon.stub(db, 'doQuery').resolves([]);
await db.createTableFromFile('sync_halt.sql');
assert.ok(doQueryStub.callCount >= 1, 'at least one statement executed from sync_halt.sql');
let statements = doQueryStub.getCalls().map(c => c.args[0]);
assert.ok(statements.some(s => /CREATE TABLE/i.test(s)),
'a CREATE TABLE statement was executed from sync_halt.sql, got: ' + JSON.stringify(statements));
});
});
Loading