From b37820798793669027452fb4ef0f0a6e7ea6b73d Mon Sep 17 00:00:00 2001 From: Abraham Ingersoll <586805+aberoham@users.noreply.github.com> Date: Sat, 29 Aug 2026 10:18:56 +0100 Subject: [PATCH 1/4] sql: re-seed lookup rows on upgrade A database created by 2.x has the nameserver export types and record types of its day; nothing in sql/upgrade added the ones v3 introduced, so an upgraded install cannot create a coredns or native nameserver (lib/nameserver/test/runtime.js fails with "unknown nameserver type: native" on such a database). The header claims an id a site already uses keeps its own row and skips the one below it. A test runs that claim rather than asserting it. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RxcUNWRPrY7gKukije8hFC Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014AUjj218gbhUHLi7kvDPtd --- sql/schema.test.js | 47 +++++++++++++++++++++++ sql/upgrade/06_reseed_lookup_tables.sql | 51 +++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 sql/upgrade/06_reseed_lookup_tables.sql diff --git a/sql/schema.test.js b/sql/schema.test.js index c3f8b3c..e6f35b7 100644 --- a/sql/schema.test.js +++ b/sql/schema.test.js @@ -178,4 +178,51 @@ describe('sql schema', () => { const sql = await fs.readFile(path.join(sqlDir, '12_nt_options.sql'), 'utf8') assert.match(sql, /'db_version','2\.41'/) }) + + it('re-seeds every lookup row on upgrade', async () => { + const seeds = async (file) => { + const sql = await fs.readFile(path.join(sqlDir, file), 'utf8') + const rows = {} + for (const [, table, body] of sql.matchAll(/INSERT IGNORE INTO `(\w+)`[^;]*VALUES([^;]*);/g)) { + rows[table] = [...body.matchAll(/\((\d+),'([^']+)'/g)].map((m) => `${m[1]}:${m[2]}`).sort() + } + return rows + } + const upgrade = await seeds('upgrade/06_reseed_lookup_tables.sql') + const installed = { + ...(await seeds('04_nt_nameserver.sql')), + ...(await seeds('06_resource_records.sql')), + } + for (const table of ['nt_nameserver_export_type', 'resource_record_type']) { + assert.deepEqual(upgrade[table], installed[table], `${table} rows an upgraded database would never get`) + } + }) + + // The header claims an id already in use keeps its row. Run that, don't assert it. + it('leaves an id a site already uses alone', async () => { + const reseed = await fs.readFile(path.join(sqlDir, 'upgrade', '06_reseed_lookup_tables.sql'), 'utf8') + const typeRows = async () => + (await conn.query('SELECT id, name FROM nt_nameserver_export_type WHERE id = 9'))[0] + + try { + await conn.query('DELETE FROM nt_nameserver_export_type WHERE id = 9') + await conn.query( + "INSERT INTO nt_nameserver_export_type (id, name, descr, url) VALUES (9,'gerbil','Gerbil DNS','')", + ) + await conn.query(reseed) + + assert.deepEqual( + await typeRows(), + [{ id: 9, name: 'gerbil' }], + 'the reseed overwrote a row it did not own', + ) + const coredns = (await conn.query("SELECT id FROM nt_nameserver_export_type WHERE name = 'coredns'"))[0] + assert.deepEqual(coredns, [], 'coredns landed somewhere the file does not claim') + } finally { + await conn.query('DELETE FROM nt_nameserver_export_type WHERE id = 9') + await conn.query(reseed) + } + + assert.deepEqual(await typeRows(), [{ id: 9, name: 'coredns' }], 'the reseed did not restore coredns') + }) }) diff --git a/sql/upgrade/06_reseed_lookup_tables.sql b/sql/upgrade/06_reseed_lookup_tables.sql new file mode 100644 index 0000000..3f53122 --- /dev/null +++ b/sql/upgrade/06_reseed_lookup_tables.sql @@ -0,0 +1,51 @@ +# Copyright 2004-2024 The Network People, Inc. +# +# Lookup rows added since NicTool 2.x. New installs get these from +# sql/04_nt_nameserver.sql and sql/06_resource_records.sql; a database +# created by 2.x has only the rows of its day (no coredns or native +# nameserver type), so re-seed. Safe to re-run: INSERT IGNORE keeps +# existing rows, so an id already in use keeps the row it has and the +# name below is not added. + +INSERT IGNORE INTO `nt_nameserver_export_type` (`id`, `name`, `descr`, `url`) +VALUES (1,'djbdns','djbdns (tinydns & axfrdns)','cr.yp.to/djbdns.html'), + (2,'bind','BIND (zone files)', 'www.isc.org/downloads/bind/'), + (3,'maradns','MaraDNS', 'maradns.samiam.org'), + (4,'powerdns','PowerDNS','www.powerdns.com'), + (5,'bind-nsupdate','BIND (nsupdate protocol)',''), + (6,'nsd','Name Server Daemon (NSD)','www.nlnetlabs.nl/projects/nsd/'), + (7,'dynect','DynECT Standard DNS','dyn.com/managed-dns/'), + (8,'knot','Knot DNS','www.knot-dns.cz'), + (9,'coredns','CoreDNS','coredns.io'), + (10,'native','NicTool (in-process)','nictool.com'); + +INSERT IGNORE INTO `resource_record_type` (`id`, `name`, `description`, `reverse`, `forward`, `obsolete`) +VALUES + (1,'A','Address',1,1,0), + (2,'NS','Name Server',1,1,0), + (5,'CNAME','Canonical Name',1,1,0), + (6,'SOA','Start Of Authority',0,0,0), + (12,'PTR','Pointer',1,1,0), + (13,'HINFO','Host Info',0,0,1), + (15,'MX','Mail Exchanger',0,1,0), + (16,'TXT','Text',1,1,0), + (24,'SIG','Signature',0,0,0), + (25,'KEY','Key',0,0,0), + (28,'AAAA','Address IPv6',0,1,0), + (29,'LOC','Location',0,1,0), + (30,'NXT','Next',0,0,1), + (33,'SRV','Service',0,1,0), + (35,'NAPTR','Naming Authority Pointer',1,1,0), + (39,'DNAME','Delegation Name',0,0,0), + (43,'DS','Delegation Signer',1,1,0), + (44,'SSHFP','Secure Shell Key Fingerprints',0,1,0), + (46,'RRSIG','Resource Record Signature',0,1,0), + (47,'NSEC','Next Secure',0,1,0), + (48,'DNSKEY','DNS Public Key',0,1,0), + (50,'NSEC3','Next Secure v3',0,0,0), + (51,'NSEC3PARAM','NSEC3 Parameters',0,0,0), + (99,'SPF','Sender Policy Framework',0,0,1), + (250,'TSIG','Transaction Signature',0,0,0), + (252,'AXFR',NULL,0,0,0), + (256,'URI','URI',0,1,0), + (257,'CAA','Certification Authority Authorization',0,1,0); From a867eaa08a2cf60f78fef84f2b6695e5db7a8e5d Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sat, 29 Aug 2026 11:12:51 -0700 Subject: [PATCH 2/4] update copyright year --- sql/upgrade/06_reseed_lookup_tables.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql/upgrade/06_reseed_lookup_tables.sql b/sql/upgrade/06_reseed_lookup_tables.sql index 3f53122..76254ab 100644 --- a/sql/upgrade/06_reseed_lookup_tables.sql +++ b/sql/upgrade/06_reseed_lookup_tables.sql @@ -1,4 +1,4 @@ -# Copyright 2004-2024 The Network People, Inc. +# Copyright 2004-2026 The Network People, Inc. # # Lookup rows added since NicTool 2.x. New installs get these from # sql/04_nt_nameserver.sql and sql/06_resource_records.sql; a database From 2ca9ced359cfb4919bc15990142efc91b8c87e75 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Mon, 31 Aug 2026 08:30:02 -0700 Subject: [PATCH 3/4] comment update --- sql/upgrade/06_reseed_lookup_tables.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql/upgrade/06_reseed_lookup_tables.sql b/sql/upgrade/06_reseed_lookup_tables.sql index 76254ab..ccd2fd9 100644 --- a/sql/upgrade/06_reseed_lookup_tables.sql +++ b/sql/upgrade/06_reseed_lookup_tables.sql @@ -1,6 +1,6 @@ # Copyright 2004-2026 The Network People, Inc. # -# Lookup rows added since NicTool 2.x. New installs get these from +# Lookup rows added in NicTool 3.0. New installs get these from # sql/04_nt_nameserver.sql and sql/06_resource_records.sql; a database # created by 2.x has only the rows of its day (no coredns or native # nameserver type), so re-seed. Safe to re-run: INSERT IGNORE keeps From da19535d1a3673f036bd53487426d148d4f80495 Mon Sep 17 00:00:00 2001 From: Abraham Ingersoll <586805+aberoham@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:40:05 +0100 Subject: [PATCH 4/4] sql: add nameserver types by name --- sql/schema.test.js | 93 +++++++++++-------- .../06_add_nameserver_export_types.sql | 18 ++++ sql/upgrade/06_reseed_lookup_tables.sql | 51 ---------- 3 files changed, 74 insertions(+), 88 deletions(-) create mode 100644 sql/upgrade/06_add_nameserver_export_types.sql delete mode 100644 sql/upgrade/06_reseed_lookup_tables.sql diff --git a/sql/schema.test.js b/sql/schema.test.js index e6f35b7..2d75c4f 100644 --- a/sql/schema.test.js +++ b/sql/schema.test.js @@ -179,50 +179,69 @@ describe('sql schema', () => { assert.match(sql, /'db_version','2\.41'/) }) - it('re-seeds every lookup row on upgrade', async () => { - const seeds = async (file) => { - const sql = await fs.readFile(path.join(sqlDir, file), 'utf8') - const rows = {} - for (const [, table, body] of sql.matchAll(/INSERT IGNORE INTO `(\w+)`[^;]*VALUES([^;]*);/g)) { - rows[table] = [...body.matchAll(/\((\d+),'([^']+)'/g)].map((m) => `${m[1]}:${m[2]}`).sort() + it('adds v3 nameserver types without changing existing rows', async () => { + const source = await fs.readFile( + path.join(sqlDir, 'upgrade', '06_add_nameserver_export_types.sql'), + 'utf8', + ) + const migration = source.replaceAll( + '`nt_nameserver_export_type`', + '`test_nameserver_export_type_upgrade`', + ) + const rows = async () => + ( + await conn.query('SELECT id, name, descr, url FROM test_nameserver_export_type_upgrade ORDER BY id') + )[0] + const reset = async (extra) => { + await conn.query('DELETE FROM test_nameserver_export_type_upgrade') + await conn.query( + `INSERT INTO test_nameserver_export_type_upgrade (id, name, descr, url) + SELECT id, name, descr, url FROM nt_nameserver_export_type WHERE id <= 8`, + ) + for (const row of extra) { + await conn.query( + 'INSERT INTO test_nameserver_export_type_upgrade (id, name, descr, url) VALUES (?, ?, ?, ?)', + row, + ) } - return rows } - const upgrade = await seeds('upgrade/06_reseed_lookup_tables.sql') - const installed = { - ...(await seeds('04_nt_nameserver.sql')), - ...(await seeds('06_resource_records.sql')), - } - for (const table of ['nt_nameserver_export_type', 'resource_record_type']) { - assert.deepEqual(upgrade[table], installed[table], `${table} rows an upgraded database would never get`) - } - }) - - // The header claims an id already in use keeps its row. Run that, don't assert it. - it('leaves an id a site already uses alone', async () => { - const reseed = await fs.readFile(path.join(sqlDir, 'upgrade', '06_reseed_lookup_tables.sql'), 'utf8') - const typeRows = async () => - (await conn.query('SELECT id, name FROM nt_nameserver_export_type WHERE id = 9'))[0] + await conn.query('DROP TABLE IF EXISTS test_nameserver_export_type_upgrade') + await conn.query('CREATE TABLE test_nameserver_export_type_upgrade LIKE nt_nameserver_export_type') try { - await conn.query('DELETE FROM nt_nameserver_export_type WHERE id = 9') - await conn.query( - "INSERT INTO nt_nameserver_export_type (id, name, descr, url) VALUES (9,'gerbil','Gerbil DNS','')", - ) - await conn.query(reseed) - + await reset([ + [9, 'gerbil', 'Gerbil DNS', ''], + [10, 'otter', 'Otter DNS', ''], + ]) + await conn.query(migration) + + const clashingIds = await rows() + assert.deepEqual(clashingIds.slice(8, 10), [ + { id: 9, name: 'gerbil', descr: 'Gerbil DNS', url: '' }, + { id: 10, name: 'otter', descr: 'Otter DNS', url: '' }, + ]) + assert.equal(clashingIds.filter((r) => r.name === 'coredns').length, 1) + assert.equal(clashingIds.filter((r) => r.name === 'native').length, 1) + + await conn.query(migration) + assert.deepEqual(await rows(), clashingIds, 'repeat migration changed the rows') + + await reset([ + [11, 'coredns', 'Custom CoreDNS', ''], + [12, 'native', 'Custom native', ''], + ]) + await conn.query(migration) + + const existingNames = await rows() assert.deepEqual( - await typeRows(), - [{ id: 9, name: 'gerbil' }], - 'the reseed overwrote a row it did not own', + existingNames.filter((r) => ['coredns', 'native'].includes(r.name)), + [ + { id: 11, name: 'coredns', descr: 'Custom CoreDNS', url: '' }, + { id: 12, name: 'native', descr: 'Custom native', url: '' }, + ], ) - const coredns = (await conn.query("SELECT id FROM nt_nameserver_export_type WHERE name = 'coredns'"))[0] - assert.deepEqual(coredns, [], 'coredns landed somewhere the file does not claim') } finally { - await conn.query('DELETE FROM nt_nameserver_export_type WHERE id = 9') - await conn.query(reseed) + await conn.query('DROP TABLE test_nameserver_export_type_upgrade') } - - assert.deepEqual(await typeRows(), [{ id: 9, name: 'coredns' }], 'the reseed did not restore coredns') }) }) diff --git a/sql/upgrade/06_add_nameserver_export_types.sql b/sql/upgrade/06_add_nameserver_export_types.sql new file mode 100644 index 0000000..6f85cf0 --- /dev/null +++ b/sql/upgrade/06_add_nameserver_export_types.sql @@ -0,0 +1,18 @@ +# Copyright 2004-2026 The Network People, Inc. +# +# Add the nameserver export types introduced in NicTool 3.0 without +# changing existing ids. + +INSERT INTO `nt_nameserver_export_type` (`name`, `descr`, `url`) +SELECT 'coredns', 'CoreDNS', 'coredns.io' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `nt_nameserver_export_type` WHERE `name` = 'coredns' +); + +INSERT INTO `nt_nameserver_export_type` (`name`, `descr`, `url`) +SELECT 'native', 'NicTool (in-process)', 'nictool.com' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `nt_nameserver_export_type` WHERE `name` = 'native' +); diff --git a/sql/upgrade/06_reseed_lookup_tables.sql b/sql/upgrade/06_reseed_lookup_tables.sql deleted file mode 100644 index ccd2fd9..0000000 --- a/sql/upgrade/06_reseed_lookup_tables.sql +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2004-2026 The Network People, Inc. -# -# Lookup rows added in NicTool 3.0. New installs get these from -# sql/04_nt_nameserver.sql and sql/06_resource_records.sql; a database -# created by 2.x has only the rows of its day (no coredns or native -# nameserver type), so re-seed. Safe to re-run: INSERT IGNORE keeps -# existing rows, so an id already in use keeps the row it has and the -# name below is not added. - -INSERT IGNORE INTO `nt_nameserver_export_type` (`id`, `name`, `descr`, `url`) -VALUES (1,'djbdns','djbdns (tinydns & axfrdns)','cr.yp.to/djbdns.html'), - (2,'bind','BIND (zone files)', 'www.isc.org/downloads/bind/'), - (3,'maradns','MaraDNS', 'maradns.samiam.org'), - (4,'powerdns','PowerDNS','www.powerdns.com'), - (5,'bind-nsupdate','BIND (nsupdate protocol)',''), - (6,'nsd','Name Server Daemon (NSD)','www.nlnetlabs.nl/projects/nsd/'), - (7,'dynect','DynECT Standard DNS','dyn.com/managed-dns/'), - (8,'knot','Knot DNS','www.knot-dns.cz'), - (9,'coredns','CoreDNS','coredns.io'), - (10,'native','NicTool (in-process)','nictool.com'); - -INSERT IGNORE INTO `resource_record_type` (`id`, `name`, `description`, `reverse`, `forward`, `obsolete`) -VALUES - (1,'A','Address',1,1,0), - (2,'NS','Name Server',1,1,0), - (5,'CNAME','Canonical Name',1,1,0), - (6,'SOA','Start Of Authority',0,0,0), - (12,'PTR','Pointer',1,1,0), - (13,'HINFO','Host Info',0,0,1), - (15,'MX','Mail Exchanger',0,1,0), - (16,'TXT','Text',1,1,0), - (24,'SIG','Signature',0,0,0), - (25,'KEY','Key',0,0,0), - (28,'AAAA','Address IPv6',0,1,0), - (29,'LOC','Location',0,1,0), - (30,'NXT','Next',0,0,1), - (33,'SRV','Service',0,1,0), - (35,'NAPTR','Naming Authority Pointer',1,1,0), - (39,'DNAME','Delegation Name',0,0,0), - (43,'DS','Delegation Signer',1,1,0), - (44,'SSHFP','Secure Shell Key Fingerprints',0,1,0), - (46,'RRSIG','Resource Record Signature',0,1,0), - (47,'NSEC','Next Secure',0,1,0), - (48,'DNSKEY','DNS Public Key',0,1,0), - (50,'NSEC3','Next Secure v3',0,0,0), - (51,'NSEC3PARAM','NSEC3 Parameters',0,0,0), - (99,'SPF','Sender Policy Framework',0,0,1), - (250,'TSIG','Transaction Signature',0,0,0), - (252,'AXFR',NULL,0,0,0), - (256,'URI','URI',0,1,0), - (257,'CAA','Certification Authority Authorization',0,1,0);