meadow-integration 1.1.2 → 1.1.3

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.
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+
3
+ const Chai = require('chai');
4
+ const Expect = Chai.expect;
5
+
6
+ const libIndexPolicy = require('../source/services/clone/Meadow-Service-IndexPolicy.js');
7
+
8
+ const _DocumentSchema =
9
+ {
10
+ TableName: 'Document',
11
+ DefaultIdentifier: 'IDDocument',
12
+ Columns:
13
+ [
14
+ { Column: 'IDDocument', DataType: 'ID' },
15
+ { Column: 'GUIDDocument', DataType: 'GUID' },
16
+ { Column: 'IDProject', DataType: 'ForeignKey' },
17
+ { Column: 'Name', DataType: 'String', Size: 128 },
18
+ { Column: 'Deleted', DataType: 'Boolean' }
19
+ ]
20
+ };
21
+
22
+ // A join table with no Deleted column and no GUID column.
23
+ const _JoinSchema =
24
+ {
25
+ TableName: 'DocumentPolyJoin',
26
+ DefaultIdentifier: 'IDDocumentPolyJoin',
27
+ Columns:
28
+ [
29
+ { Column: 'IDDocumentPolyJoin', DataType: 'ID' },
30
+ { Column: 'IDDocument', DataType: 'ForeignKey' }
31
+ ]
32
+ };
33
+
34
+ const byName = (pIndices, pName) => pIndices.find((pIndex) => { return pIndex.Name === pName; });
35
+
36
+ suite('Meadow Integration - IndexPolicy',
37
+ () =>
38
+ {
39
+ suite('standard operational indexes',
40
+ () =>
41
+ {
42
+ test('emits a NON-UNIQUE (Deleted, ID) composite for a soft-deletable table',
43
+ () =>
44
+ {
45
+ let tmpIndices = libIndexPolicy.resolveDesiredIndexes(_DocumentSchema, {});
46
+ let tmpComposite = byName(tmpIndices, 'IX_M_SYNC_Document_Deleted_IDDocument');
47
+ Expect(tmpComposite, 'composite present').to.be.an('object');
48
+ Expect(tmpComposite.Columns).to.deep.equal([ 'Deleted', 'IDDocument' ]);
49
+ Expect(tmpComposite.Unique).to.equal(false);
50
+ });
51
+
52
+ test('emits a NON-UNIQUE GUID lookup index',
53
+ () =>
54
+ {
55
+ let tmpIndices = libIndexPolicy.resolveDesiredIndexes(_DocumentSchema, {});
56
+ let tmpGUID = byName(tmpIndices, 'IX_M_SYNC_Document_GUIDDocument');
57
+ Expect(tmpGUID, 'GUID index present').to.be.an('object');
58
+ Expect(tmpGUID.Columns).to.deep.equal([ 'GUIDDocument' ]);
59
+ Expect(tmpGUID.Unique).to.equal(false);
60
+ });
61
+
62
+ test('emits nothing standard for a table with neither Deleted nor GUID',
63
+ () =>
64
+ {
65
+ let tmpIndices = libIndexPolicy.resolveDesiredIndexes(_JoinSchema, {});
66
+ Expect(tmpIndices.filter((pIndex) => { return pIndex.Name.indexOf('IX_M_SYNC_') === 0; })).to.have.length(0);
67
+ });
68
+
69
+ test('StandardOperationalIndexes:false opts out of the standard set',
70
+ () =>
71
+ {
72
+ let tmpIndices = libIndexPolicy.resolveDesiredIndexes(_DocumentSchema, { StandardOperationalIndexes: false });
73
+ Expect(tmpIndices).to.have.length(0);
74
+ });
75
+ });
76
+
77
+ suite('caller-declared per-table extras',
78
+ () =>
79
+ {
80
+ test('adds a configured extra index for the target table only',
81
+ () =>
82
+ {
83
+ let tmpConfig = { TableIndexes: { Document: [ { Columns: [ 'IDProject', 'Deleted' ] } ] } };
84
+ let tmpDoc = libIndexPolicy.resolveDesiredIndexes(_DocumentSchema, tmpConfig);
85
+ let tmpExtra = byName(tmpDoc, 'IX_M_SYNC_Document_IDProject_Deleted');
86
+ Expect(tmpExtra, 'configured extra present').to.be.an('object');
87
+ Expect(tmpExtra.Columns).to.deep.equal([ 'IDProject', 'Deleted' ]);
88
+
89
+ // A different table gets no extras from that config.
90
+ let tmpJoin = libIndexPolicy.resolveDesiredIndexes(_JoinSchema, tmpConfig);
91
+ Expect(tmpJoin.some((pIndex) => { return pIndex.Name.indexOf('IX_M_SYNC_DocumentPolyJoin_ID') === 0; })).to.equal(false);
92
+ });
93
+
94
+ test('honors an explicit Name and Unique on a configured extra',
95
+ () =>
96
+ {
97
+ let tmpConfig = { TableIndexes: { Document: [ { Name: 'IX_custom_docname', Columns: 'Name', Unique: true } ] } };
98
+ let tmpDoc = libIndexPolicy.resolveDesiredIndexes(_DocumentSchema, tmpConfig);
99
+ let tmpExtra = byName(tmpDoc, 'IX_custom_docname');
100
+ Expect(tmpExtra, 'named extra present').to.be.an('object');
101
+ Expect(tmpExtra.Columns).to.deep.equal([ 'Name' ]);
102
+ Expect(tmpExtra.Unique).to.equal(true);
103
+ });
104
+
105
+ test('skips a configured extra whose columns are not all present',
106
+ () =>
107
+ {
108
+ let tmpConfig = { TableIndexes: { Document: [ { Columns: [ 'Name', 'NonexistentColumn' ] } ] } };
109
+ let tmpDoc = libIndexPolicy.resolveDesiredIndexes(_DocumentSchema, tmpConfig);
110
+ Expect(tmpDoc.some((pIndex) => { return pIndex.Columns.indexOf('NonexistentColumn') >= 0; })).to.equal(false);
111
+ });
112
+ });
113
+
114
+ suite('isManagedIndexName (prune scope = managed)',
115
+ () =>
116
+ {
117
+ test('recognizes policy, schema-managed, and legacy column-named indexes',
118
+ () =>
119
+ {
120
+ Expect(libIndexPolicy.isManagedIndexName('IX_M_SYNC_Document_Deleted_IDDocument', _DocumentSchema)).to.equal(true);
121
+ Expect(libIndexPolicy.isManagedIndexName('AK_M_GUIDDocument', _DocumentSchema)).to.equal(true);
122
+ Expect(libIndexPolicy.isManagedIndexName('Deleted', _DocumentSchema)).to.equal(true); // precursor artifact
123
+ Expect(libIndexPolicy.isManagedIndexName('GUIDDocument', _DocumentSchema)).to.equal(true); // precursor artifact
124
+ });
125
+
126
+ test('leaves a truly external hand-authored index untouched',
127
+ () =>
128
+ {
129
+ Expect(libIndexPolicy.isManagedIndexName('IX_dba_custom_reporting', _DocumentSchema)).to.equal(false);
130
+ Expect(libIndexPolicy.isManagedIndexName('PK_Document', _DocumentSchema)).to.equal(false);
131
+ });
132
+ });
133
+ });
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Unit tests for per-record sync fan-out concurrency resolution.
3
+ *
4
+ * The sync strategies fan out record reconcile/upsert work with
5
+ * `eachLimit(records, this.SyncRecordConcurrency, ...)`. This locks in the
6
+ * resolution rules so a small clone (e.g. a 2-connection pool) never fans out
7
+ * wider than its pool, while remaining explicitly tunable.
8
+ *
9
+ * Pure JS — no DB connection needed; the pool size is read from a stubbed
10
+ * provider exposing `connectionPoolLimit`.
11
+ *
12
+ * @license MIT
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ const Chai = require('chai');
18
+ const Expect = Chai.expect;
19
+
20
+ const libFable = require('fable');
21
+ const libMeadowSyncEntityOngoing = require('../source/services/clone/Meadow-Service-Sync-Entity-Ongoing.js');
22
+ const libMeadowSyncEntityInitial = require('../source/services/clone/Meadow-Service-Sync-Entity-Initial.js');
23
+ const libSyncPoolLimit = require('../source/services/clone/Meadow-Service-Sync-PoolLimit.js');
24
+
25
+ const _MinimalEntitySchema =
26
+ {
27
+ TableName: 'Book',
28
+ Columns:
29
+ [
30
+ { Column: 'IDBook', DataType: 'ID' },
31
+ { Column: 'GUIDBook', DataType: 'GUID' },
32
+ { Column: 'Deleted', DataType: 'int' },
33
+ { Column: 'Title', DataType: 'String' }
34
+ ],
35
+ MeadowSchema:
36
+ {
37
+ Scope: 'Book',
38
+ DefaultIdentifier: 'IDBook',
39
+ Domain: 'Default',
40
+ Schema: [],
41
+ DefaultObject: {}
42
+ }
43
+ };
44
+
45
+ // Construct a sync entity directly with the given option overrides and an
46
+ // optional stubbed connection pool size on a chosen provider service name
47
+ // (defaults to the MSSQL provider).
48
+ function makeEntity(pEntityClass, pOptionOverrides, pPoolLimit, pProviderService)
49
+ {
50
+ let tmpFable = new libFable({ Product: 'MeadowSyncConcurrencyTest' });
51
+ if (typeof(pPoolLimit) === 'number')
52
+ {
53
+ // Stub the connection provider's pool-size reporter.
54
+ tmpFable[pProviderService || 'MeadowMSSQLProvider'] = { connectionPoolLimit: pPoolLimit };
55
+ }
56
+ let tmpOptions = Object.assign({ MeadowEntitySchema: _MinimalEntitySchema }, pOptionOverrides || {});
57
+ return new pEntityClass(tmpFable, tmpOptions, 'sync-concurrency-test');
58
+ }
59
+
60
+ suite('Meadow Integration - Sync Record Concurrency',
61
+ () =>
62
+ {
63
+ suite('Ongoing (shared base for OEC / TrueUp / ComparisonOnly)',
64
+ () =>
65
+ {
66
+ test('defaults to the DB connection pool size when nothing is configured',
67
+ () =>
68
+ {
69
+ let tmpEntity = makeEntity(libMeadowSyncEntityOngoing, {}, 2);
70
+ Expect(tmpEntity.SyncRecordConcurrency).to.equal(2);
71
+ });
72
+
73
+ test('falls back to 5 when no pool-reporting provider is present',
74
+ () =>
75
+ {
76
+ let tmpEntity = makeEntity(libMeadowSyncEntityOngoing, {}, undefined);
77
+ Expect(tmpEntity.SyncRecordConcurrency).to.equal(5);
78
+ });
79
+
80
+ test('explicit SyncRecordConcurrency wins over the pool size',
81
+ () =>
82
+ {
83
+ let tmpEntity = makeEntity(libMeadowSyncEntityOngoing, { SyncRecordConcurrency: 7 }, 2);
84
+ Expect(tmpEntity.SyncRecordConcurrency).to.equal(7);
85
+ });
86
+
87
+ test('a non-positive configured value is ignored in favor of the pool size',
88
+ () =>
89
+ {
90
+ let tmpEntity = makeEntity(libMeadowSyncEntityOngoing, { SyncRecordConcurrency: 0 }, 4);
91
+ Expect(tmpEntity.SyncRecordConcurrency).to.equal(4);
92
+ });
93
+
94
+ test('defaults to the pool size of an active MySQL clone target',
95
+ () =>
96
+ {
97
+ let tmpEntity = makeEntity(libMeadowSyncEntityOngoing, {}, 8, 'MeadowMySQLProvider');
98
+ Expect(tmpEntity.SyncRecordConcurrency).to.equal(8);
99
+ });
100
+
101
+ test('defaults to the pool size of an active PostgreSQL clone target',
102
+ () =>
103
+ {
104
+ let tmpEntity = makeEntity(libMeadowSyncEntityOngoing, {}, 6, 'MeadowPostgreSQLProvider');
105
+ Expect(tmpEntity.SyncRecordConcurrency).to.equal(6);
106
+ });
107
+
108
+ test('caps the auto-derived default for a very large pool',
109
+ () =>
110
+ {
111
+ let tmpEntity = makeEntity(libMeadowSyncEntityOngoing, {}, 100);
112
+ Expect(tmpEntity.SyncRecordConcurrency).to.equal(libSyncPoolLimit.MAX_DEFAULT_CONCURRENCY);
113
+ });
114
+
115
+ test('an explicit concurrency above the cap is honored (the cap only tempers the default)',
116
+ () =>
117
+ {
118
+ let tmpEntity = makeEntity(libMeadowSyncEntityOngoing, { SyncRecordConcurrency: 50 }, 100);
119
+ Expect(tmpEntity.SyncRecordConcurrency).to.equal(50);
120
+ });
121
+ });
122
+
123
+ suite('Initial (separate base)',
124
+ () =>
125
+ {
126
+ test('defaults to the DB connection pool size',
127
+ () =>
128
+ {
129
+ let tmpEntity = makeEntity(libMeadowSyncEntityInitial, {}, 3);
130
+ Expect(tmpEntity.SyncRecordConcurrency).to.equal(3);
131
+ });
132
+
133
+ test('explicit config wins over the pool size',
134
+ () =>
135
+ {
136
+ let tmpEntity = makeEntity(libMeadowSyncEntityInitial, { SyncRecordConcurrency: 9 }, 3);
137
+ Expect(tmpEntity.SyncRecordConcurrency).to.equal(9);
138
+ });
139
+ });
140
+
141
+ // The shared resolver used by all three fan-out sites (both entity bases
142
+ // + the orchestrator's table-init loop).
143
+ suite('resolveConnectionPoolLimit (shared helper)',
144
+ () =>
145
+ {
146
+ test('returns the pool size of the active MSSQL/MySQL/PostgreSQL provider',
147
+ () =>
148
+ {
149
+ Expect(libSyncPoolLimit.resolveConnectionPoolLimit({ MeadowMSSQLProvider: { connectionPoolLimit: 2 } })).to.equal(2);
150
+ Expect(libSyncPoolLimit.resolveConnectionPoolLimit({ MeadowMySQLProvider: { connectionPoolLimit: 8 } })).to.equal(8);
151
+ Expect(libSyncPoolLimit.resolveConnectionPoolLimit({ MeadowPostgreSQLProvider: { connectionPoolLimit: 6 } })).to.equal(6);
152
+ });
153
+
154
+ test('returns 0 when no pooled provider is registered (e.g. SQLite)',
155
+ () =>
156
+ {
157
+ Expect(libSyncPoolLimit.resolveConnectionPoolLimit({ MeadowSQLiteProvider: {} })).to.equal(0);
158
+ Expect(libSyncPoolLimit.resolveConnectionPoolLimit({})).to.equal(0);
159
+ Expect(libSyncPoolLimit.resolveConnectionPoolLimit(null)).to.equal(0);
160
+ });
161
+
162
+ test('ignores a provider that reports a non-positive or non-numeric pool size',
163
+ () =>
164
+ {
165
+ Expect(libSyncPoolLimit.resolveConnectionPoolLimit({ MeadowMSSQLProvider: { connectionPoolLimit: 0 } })).to.equal(0);
166
+ Expect(libSyncPoolLimit.resolveConnectionPoolLimit({ MeadowMySQLProvider: { connectionPoolLimit: 'lots' } })).to.equal(0);
167
+ });
168
+
169
+ test('resolveDefaultConcurrency sizes to the pool but caps very large pools', () =>
170
+ {
171
+ Expect(libSyncPoolLimit.resolveDefaultConcurrency({ MeadowMSSQLProvider: { connectionPoolLimit: 3 } })).to.equal(3);
172
+ Expect(libSyncPoolLimit.resolveDefaultConcurrency({ MeadowMySQLProvider: { connectionPoolLimit: 1000 } })).to.equal(libSyncPoolLimit.MAX_DEFAULT_CONCURRENCY);
173
+ });
174
+
175
+ test('resolveDefaultConcurrency falls back to the default when no pool is reported', () =>
176
+ {
177
+ Expect(libSyncPoolLimit.resolveDefaultConcurrency({})).to.equal(libSyncPoolLimit.DEFAULT_CONCURRENCY);
178
+ Expect(libSyncPoolLimit.resolveDefaultConcurrency(null)).to.equal(libSyncPoolLimit.DEFAULT_CONCURRENCY);
179
+ });
180
+ });
181
+ });