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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meadow-integration",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "Meadow Data Integration",
5
5
  "retoldBeacon": {
6
6
  "displayName": "Meadow Integration",
@@ -58,7 +58,7 @@
58
58
  "author": "steven velozo <steven@velozo.com>",
59
59
  "license": "MIT",
60
60
  "devDependencies": {
61
- "meadow-connection-sqlite": "^1.0.20",
61
+ "meadow-connection-sqlite": "^1.0.21",
62
62
  "pict-docuserve": "^1.4.19",
63
63
  "quackage": "^1.3.0",
64
64
  "retold-harness": "^1.1.13"
@@ -82,19 +82,19 @@
82
82
  ]
83
83
  },
84
84
  "dependencies": {
85
- "fable": "^3.1.75",
85
+ "fable": "^3.1.79",
86
86
  "fable-serviceproviderbase": "^3.0.19",
87
87
  "fast-xml-parser": "^4.4.1",
88
- "meadow": "^2.0.43",
89
- "meadow-connection-mssql": "^1.0.23",
90
- "meadow-connection-mysql": "^1.0.19",
88
+ "meadow": "^2.0.47",
89
+ "meadow-connection-mssql": "^1.0.24",
90
+ "meadow-connection-mysql": "^1.0.20",
91
91
  "orator": "^6.1.2",
92
92
  "orator-serviceserver-restify": "^2.0.11",
93
93
  "pict-provider-theme": "^1.1.2",
94
94
  "pict-section-flow": "^1.0.1",
95
- "pict-section-modal": "^1.1.4",
95
+ "pict-section-modal": "^1.3.2",
96
96
  "pict-section-theme": "^1.1.1",
97
- "pict-service-commandlineutility": "^1.0.19",
97
+ "pict-service-commandlineutility": "^1.0.20",
98
98
  "pict-sessionmanager": "^1.0.2",
99
99
  "pict-view": "^1.0.68",
100
100
  "xlsx": "^0.18.5"
@@ -7,6 +7,8 @@ const libMeadowConnectionManager = require('../../services/clone/Meadow-Service-
7
7
  const libMeadowCloneRestClient = require('../../services/clone/Meadow-Service-RestClient.js');
8
8
  const libMeadowSync = require('../../services/clone/Meadow-Service-Sync.js');
9
9
  const libSessionManagerSetup = require('../../Meadow-Integration-SessionManagerSetup.js');
10
+ const libIndexPolicy = require('../../services/clone/Meadow-Service-IndexPolicy.js');
11
+ const libIndexConvergence = require('../../services/clone/Meadow-Service-IndexConvergence.js');
10
12
 
11
13
  // Resolve an env var with the standard `_FILE` suffix fallback for
12
14
  // secrets — so docker / k8s secret mounts work without bespoke wiring.
@@ -65,6 +67,8 @@ class DataClone extends libCLICommandLineCommand
65
67
 
66
68
  this.options.CommandOptions.push({ Name: '-w, --post_run_delay [post_run_delay]', Description: 'Minutes to wait after sync before exiting. Default is 0.', DefaultValue: '0' });
67
69
 
70
+ this.options.CommandOptions.push({ Name: '--index_config [index_config]', Description: 'Converge clone indexes before syncing. "all" applies the standard operational indexes to every table; or pass a path to a JSON index-policy file ({ StandardOperationalIndexes, PruneScope, TableIndexes }). Omit to leave indexes untouched.' });
71
+
68
72
  this.addCommand();
69
73
  }
70
74
 
@@ -373,6 +377,11 @@ class DataClone extends libCLICommandLineCommand
373
377
  });
374
378
  },
375
379
  (fStageComplete) =>
380
+ {
381
+ // Converge indexes to the policy before syncing (opt-in via --index_config).
382
+ this._convergeIndexes(tmpSchemaModel, fStageComplete);
383
+ },
384
+ (fStageComplete) =>
376
385
  {
377
386
  // Execute the sync
378
387
  this.fable.MeadowSync.syncAll(
@@ -405,6 +414,85 @@ class DataClone extends libCLICommandLineCommand
405
414
  }, tmpPostRunDelay * 60 * 1000);
406
415
  });
407
416
  }
417
+
418
+ /**
419
+ * Converge the clone's indexes to the configured policy before syncing.
420
+ * Opt-in via --index_config: "all" applies the standard operational indexes
421
+ * to every table; a path loads a JSON index-policy file. Uses the same
422
+ * IndexPolicy + IndexConvergence the headless DataCloner pipeline uses.
423
+ *
424
+ * @param {object} pSchemaModel - loaded Meadow schema ({ Tables: {...} })
425
+ * @param {Function} fCallback
426
+ */
427
+ _convergeIndexes(pSchemaModel, fCallback)
428
+ {
429
+ let tmpIndexConfigArg = this.CommandOptions.index_config;
430
+ if (!tmpIndexConfigArg)
431
+ {
432
+ return fCallback();
433
+ }
434
+
435
+ let tmpIndexConfig;
436
+ if (String(tmpIndexConfigArg).toLowerCase() === 'all')
437
+ {
438
+ tmpIndexConfig = { StandardOperationalIndexes: true, PruneScope: 'managed' };
439
+ }
440
+ else
441
+ {
442
+ try
443
+ {
444
+ tmpIndexConfig = require(libPath.resolve(tmpIndexConfigArg));
445
+ }
446
+ catch (pReadError)
447
+ {
448
+ this.log.error(`Could not load --index_config ${tmpIndexConfigArg}: ${pReadError.message}`);
449
+ return fCallback();
450
+ }
451
+ }
452
+
453
+ let tmpProviderName = this.fable.MeadowConnectionManager && this.fable.MeadowConnectionManager.Provider;
454
+ let tmpProvider = tmpProviderName ? this.fable[`Meadow${tmpProviderName}Provider`] : null;
455
+ if (!tmpProvider || typeof(tmpProvider.introspectTableIndices) !== 'function')
456
+ {
457
+ this.log.warn(`Index convergence requested but provider '${tmpProviderName || '(unknown)'}' does not expose the index API; skipping.`);
458
+ return fCallback();
459
+ }
460
+
461
+ let tmpTables = (pSchemaModel && pSchemaModel.Tables) ? pSchemaModel.Tables : {};
462
+ // Converge only the tables actually deployed — respect SyncEntityList (a
463
+ // subset clone) so we never introspect/converge a table that was never
464
+ // created. Mirrors loadMeadowSchema's filter and the headless endpoint's
465
+ // "deployed tables only" scope. Empty list = all tables in the schema.
466
+ let tmpSyncList = (this.fable.MeadowSync && Array.isArray(this.fable.MeadowSync.SyncEntityList)) ? this.fable.MeadowSync.SyncEntityList : [];
467
+ let tmpTableNames = (tmpSyncList.length > 0)
468
+ ? tmpSyncList.filter((pName) => { return tmpTables[pName]; })
469
+ : Object.keys(tmpTables);
470
+ let tmpPruneScope = tmpIndexConfig.PruneScope || libIndexConvergence.PRUNE_MANAGED;
471
+
472
+ this.log.info(`Converging indexes for ${tmpTableNames.length} deployed table(s) (prune scope: ${tmpPruneScope})...`);
473
+
474
+ this.fable.Utility.eachLimit(tmpTableNames, 1,
475
+ (pTableName, fNextTable) =>
476
+ {
477
+ let tmpTableSchema = tmpTables[pTableName];
478
+ let tmpDesired = libIndexPolicy.resolveDesiredIndexes(tmpTableSchema, tmpIndexConfig);
479
+ libIndexConvergence.convergeTableIndexes(tmpProvider, tmpTableSchema, tmpDesired,
480
+ { PruneScope: tmpPruneScope, log: this.log },
481
+ (pConvergeError, pResult) =>
482
+ {
483
+ if (pConvergeError)
484
+ {
485
+ this.log.warn(`Index convergence for ${pTableName} failed: ${pConvergeError}`);
486
+ }
487
+ else if (pResult && (pResult.created.length > 0 || pResult.dropped.length > 0))
488
+ {
489
+ this.log.info(`${pTableName} — created [${pResult.created.join(', ')}], dropped [${pResult.dropped.join(', ')}]`);
490
+ }
491
+ return fNextTable();
492
+ });
493
+ },
494
+ () => { return fCallback(); });
495
+ }
408
496
  }
409
497
 
410
498
  module.exports = DataClone;
@@ -73,6 +73,15 @@ class MeadowConnectionManager extends libFableServiceProviderBase
73
73
  // Apply MySQL settings to fable settings for meadow provider
74
74
  this.fable.settings.MySQL = tmpConfig;
75
75
 
76
+ // Point the meadow ORM at this provider. MeadowConnectionManager
77
+ // connects the connector (fable.MeadowMSSQLProvider/MySQLProvider),
78
+ // but the meadow instance built in Meadow-Service-Sync selects its
79
+ // provider from fable.settings.MeadowProvider (default 'None').
80
+ // Without this the ORM stays on the 'None' provider and createTable
81
+ // (via provider.getProvider()) has nothing to call. Set before the
82
+ // sync service is instantiated so meadow picks it up.
83
+ this.fable.settings.MeadowProvider = this.Provider;
84
+
76
85
  this.fable.serviceManager.addServiceType('MeadowMySQLProvider', libMeadowConnectionMySQL);
77
86
  this.fable.serviceManager.instantiateServiceProvider('MeadowMySQLProvider', tmpConfig);
78
87
 
@@ -108,6 +117,11 @@ class MeadowConnectionManager extends libFableServiceProviderBase
108
117
  // Apply MSSQL settings to fable settings for meadow provider
109
118
  this.fable.settings.MSSQL = tmpConfig;
110
119
 
120
+ // Point the meadow ORM at this provider (see the MySQL branch for
121
+ // the full rationale) — without it the ORM stays on 'None' and
122
+ // createTable has no underlying connector to call.
123
+ this.fable.settings.MeadowProvider = this.Provider;
124
+
111
125
  this.fable.serviceManager.addServiceType('MeadowMSSQLProvider', libMeadowConnectionMSSQL);
112
126
  this.fable.serviceManager.instantiateServiceProvider('MeadowMSSQLProvider', tmpConfig);
113
127
 
@@ -0,0 +1,181 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Index convergence for a clone table.
5
+ *
6
+ * Given a table's DESIRED index set (from Meadow-Service-IndexPolicy) and a
7
+ * connection provider, bring the table's actual indexes in line with the
8
+ * desired set:
9
+ *
10
+ * introspect actual → diff by name → create missing → drop undeclared
11
+ *
12
+ * Two safety properties:
13
+ * 1. Create-before-drop: new indexes are built BEFORE any old one is removed,
14
+ * so there is never a window with no covering index (matters because the
15
+ * sync's range-count walk runs right after).
16
+ * 2. No-loss-on-failure: if any desired index fails to build this run, NO
17
+ * drops happen — we never remove a still-needed index whose replacement is
18
+ * not yet in place. The drop is retried next run once the create succeeds.
19
+ *
20
+ * Prune scope controls what may be dropped:
21
+ * - 'none' : additive only (never drop).
22
+ * - 'managed' : drop only indexes the policy owns or legacy artifacts
23
+ * (see IndexPolicy.isManagedIndexName) — a truly external,
24
+ * hand-authored index is left alone. DEFAULT.
25
+ * - 'all' : drop any undeclared index. (introspectTableIndices already
26
+ * excludes the primary key, so the PK is never a drop target.)
27
+ *
28
+ * @license MIT
29
+ */
30
+
31
+ const libIndexPolicy = require('./Meadow-Service-IndexPolicy.js');
32
+
33
+ const PRUNE_NONE = 'none';
34
+ const PRUNE_MANAGED = 'managed';
35
+ const PRUNE_ALL = 'all';
36
+
37
+ const NOOP_LOG = { info: () => {}, warn: () => {}, error: () => {} };
38
+
39
+ /**
40
+ * Converge a single table's indexes to the desired set.
41
+ *
42
+ * @param {object} pProvider - connection provider exposing introspectTableIndices,
43
+ * generateCreateIndexStatements, createIndex, dropIndex
44
+ * @param {object} pTableSchema - { TableName, Columns[] }
45
+ * @param {Array} pDesiredIndexes - desired defs ({ Name, TableName, Columns[], Unique, Strategy })
46
+ * @param {object} [pOptions] - { PruneScope?: string, log?: object }
47
+ * @param {Function} fCallback - callback(pError, { table, created[], dropped[], skipped[] })
48
+ */
49
+ function convergeTableIndexes(pProvider, pTableSchema, pDesiredIndexes, pOptions, fCallback)
50
+ {
51
+ let tmpOptions = pOptions || {};
52
+ let tmpLog = tmpOptions.log || NOOP_LOG;
53
+ let tmpPruneScope = tmpOptions.PruneScope || PRUNE_MANAGED;
54
+ let tmpTableName = pTableSchema.TableName;
55
+ let tmpDesired = Array.isArray(pDesiredIndexes) ? pDesiredIndexes : [];
56
+
57
+ if (!pProvider
58
+ || typeof(pProvider.introspectTableIndices) !== 'function'
59
+ || typeof(pProvider.generateCreateIndexStatements) !== 'function'
60
+ || typeof(pProvider.createIndex) !== 'function'
61
+ || typeof(pProvider.dropIndex) !== 'function')
62
+ {
63
+ return fCallback(new Error('IndexConvergence requires a provider with introspectTableIndices / generateCreateIndexStatements / createIndex / dropIndex'));
64
+ }
65
+
66
+ pProvider.introspectTableIndices(tmpTableName,
67
+ (pIntrospectError, pActualIndices) =>
68
+ {
69
+ if (pIntrospectError)
70
+ {
71
+ return fCallback(pIntrospectError);
72
+ }
73
+
74
+ let tmpActual = Array.isArray(pActualIndices) ? pActualIndices : [];
75
+ let tmpActualNames = new Set(tmpActual.map((pIndex) => { return pIndex.Name; }));
76
+ let tmpDesiredNames = new Set(tmpDesired.map((pIndex) => { return pIndex.Name; }));
77
+
78
+ // Missing desired indexes to create.
79
+ let tmpToCreate = tmpDesired.filter((pIndex) => { return !tmpActualNames.has(pIndex.Name); });
80
+
81
+ // Undeclared actual indexes to drop, scoped by prune policy.
82
+ let tmpToDrop = [];
83
+ if (tmpPruneScope !== PRUNE_NONE)
84
+ {
85
+ tmpToDrop = tmpActual.filter((pIndex) =>
86
+ {
87
+ if (tmpDesiredNames.has(pIndex.Name))
88
+ {
89
+ return false;
90
+ }
91
+ if (tmpPruneScope === PRUNE_ALL)
92
+ {
93
+ return true;
94
+ }
95
+ return libIndexPolicy.isManagedIndexName(pIndex.Name, pTableSchema);
96
+ });
97
+ }
98
+
99
+ let tmpResult = { table: tmpTableName, created: [], dropped: [], skipped: [] };
100
+
101
+ // Render CREATE statements for exactly the to-create set. Passing
102
+ // Columns:[] means the connector's per-column auto-derivation (GUID /
103
+ // FK / Indexed) contributes nothing — only the explicit Indices[] we
104
+ // hand it are rendered.
105
+ let tmpCreateStatements = pProvider.generateCreateIndexStatements(
106
+ { TableName: tmpTableName, Columns: [], Indices: tmpToCreate });
107
+
108
+ // -- Phase B: drop (runs after all creates) --
109
+ let fDropPhase = (pCreateFailures) =>
110
+ {
111
+ if (tmpToDrop.length < 1)
112
+ {
113
+ return fCallback(null, tmpResult);
114
+ }
115
+ if (pCreateFailures > 0)
116
+ {
117
+ tmpLog.warn(`IndexConvergence: ${pCreateFailures} create(s) failed on ${tmpTableName}; skipping ${tmpToDrop.length} drop(s) this run to preserve coverage.`);
118
+ for (let i = 0; i < tmpToDrop.length; i++)
119
+ {
120
+ tmpResult.skipped.push(tmpToDrop[i].Name);
121
+ }
122
+ return fCallback(null, tmpResult);
123
+ }
124
+
125
+ let fDropNext = (pIndex) =>
126
+ {
127
+ if (pIndex >= tmpToDrop.length)
128
+ {
129
+ return fCallback(null, tmpResult);
130
+ }
131
+ let tmpDropName = tmpToDrop[pIndex].Name;
132
+ pProvider.dropIndex(tmpTableName, tmpDropName,
133
+ (pDropError) =>
134
+ {
135
+ if (pDropError)
136
+ {
137
+ tmpLog.warn(`IndexConvergence: drop ${tmpDropName} on ${tmpTableName} failed: ${pDropError}`);
138
+ tmpResult.skipped.push(tmpDropName);
139
+ }
140
+ else
141
+ {
142
+ tmpResult.dropped.push(tmpDropName);
143
+ }
144
+ return fDropNext(pIndex + 1);
145
+ });
146
+ };
147
+ fDropNext(0);
148
+ };
149
+
150
+ // -- Phase A: create --
151
+ let fCreateNext = (pIndex, pCreateFailures) =>
152
+ {
153
+ if (pIndex >= tmpCreateStatements.length)
154
+ {
155
+ return fDropPhase(pCreateFailures);
156
+ }
157
+ let tmpStatement = tmpCreateStatements[pIndex];
158
+ pProvider.createIndex(tmpStatement,
159
+ (pCreateError) =>
160
+ {
161
+ if (pCreateError)
162
+ {
163
+ tmpLog.warn(`IndexConvergence: create ${tmpStatement.Name} on ${tmpTableName} failed: ${pCreateError}`);
164
+ tmpResult.skipped.push(tmpStatement.Name);
165
+ return fCreateNext(pIndex + 1, pCreateFailures + 1);
166
+ }
167
+ tmpResult.created.push(tmpStatement.Name);
168
+ return fCreateNext(pIndex + 1, pCreateFailures);
169
+ });
170
+ };
171
+
172
+ fCreateNext(0, 0);
173
+ });
174
+ }
175
+
176
+ module.exports = {
177
+ PRUNE_NONE: PRUNE_NONE,
178
+ PRUNE_MANAGED: PRUNE_MANAGED,
179
+ PRUNE_ALL: PRUNE_ALL,
180
+ convergeTableIndexes: convergeTableIndexes
181
+ };
@@ -0,0 +1,165 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Index policy for the clone/sync workload.
5
+ *
6
+ * This is the single source of truth for which indexes a synced table SHOULD
7
+ * have, independent of any one execution flow. Both the headless DataCloner
8
+ * pipeline and the standalone CLI clone tool resolve their desired index set
9
+ * from here so they converge to the same shape.
10
+ *
11
+ * The desired set for a table is the union of:
12
+ * 1. Standard operational indexes (applied to every eligible table):
13
+ * - (Deleted, ID<Table>) composite — makes the OngoingEventualConsistency
14
+ * range-count / delete-reconcile walk a seek instead of a scan.
15
+ * - a GUID lookup index — speeds meadow's app-side GUID
16
+ * precheck on insert. NON-UNIQUE by policy: the clone is a derived
17
+ * replica of data with known duplicate GUIDs at the source, and a
18
+ * unique index would fail to build on a table that holds a dup pair.
19
+ * 2. Caller-declared per-table indexes (the "observed required" extras):
20
+ * config.TableIndexes[<TableName>] = [ { Columns, Unique?, Name?, Strategy? } ]
21
+ *
22
+ * Every policy-produced index carries the IX_M_SYNC_ prefix so the convergence
23
+ * pass can recognize the set it manages. Definitions match the shape the
24
+ * connectors' generateCreateIndexStatements consumes:
25
+ * { Name, TableName, Columns[], Unique, Strategy }
26
+ *
27
+ * @license MIT
28
+ */
29
+
30
+ // All policy-managed indexes share this prefix so convergence can scope itself.
31
+ const INDEX_POLICY_PREFIX = 'IX_M_SYNC_';
32
+
33
+ /**
34
+ * Resolve the identity/PK column for a table.
35
+ * Prefers the schema's DefaultIdentifier, then an AutoIdentity/ID column, then
36
+ * the ID<Table> naming convention.
37
+ *
38
+ * @param {object} pTableSchema
39
+ * @param {Set<string>} pColumnNames
40
+ * @returns {string}
41
+ */
42
+ function resolveIdentityColumn(pTableSchema, pColumnNames)
43
+ {
44
+ let tmpIdentity = pTableSchema.DefaultIdentifier;
45
+ if (tmpIdentity && pColumnNames.has(tmpIdentity))
46
+ {
47
+ return tmpIdentity;
48
+ }
49
+ let tmpColumns = Array.isArray(pTableSchema.Columns) ? pTableSchema.Columns : [];
50
+ let tmpIdentityCol = tmpColumns.find((pColumn) => { return (pColumn.DataType === 'AutoIdentity' || pColumn.DataType === 'ID'); });
51
+ return tmpIdentityCol ? tmpIdentityCol.Column : `ID${pTableSchema.TableName}`;
52
+ }
53
+
54
+ /**
55
+ * Resolve the desired index definitions for a single table.
56
+ *
57
+ * @param {object} pTableSchema - Meadow table schema ({ TableName, Columns[], DefaultIdentifier })
58
+ * @param {object} [pIndexConfig] - { StandardOperationalIndexes?: boolean, TableIndexes?: { [table]: Array } }
59
+ * @returns {Array<{Name:string,TableName:string,Columns:string[],Unique:boolean,Strategy:string}>}
60
+ */
61
+ function resolveDesiredIndexes(pTableSchema, pIndexConfig)
62
+ {
63
+ let tmpConfig = pIndexConfig || {};
64
+ let tmpTableName = pTableSchema.TableName;
65
+ let tmpColumns = Array.isArray(pTableSchema.Columns) ? pTableSchema.Columns : [];
66
+ let tmpColumnNames = new Set(tmpColumns.map((pColumn) => { return pColumn.Column; }));
67
+ let tmpIndices = [];
68
+
69
+ // -- 1. Standard operational indexes (all eligible tables) --
70
+ // Enabled by default; set StandardOperationalIndexes:false to opt a run out.
71
+ if (tmpConfig.StandardOperationalIndexes !== false)
72
+ {
73
+ let tmpIdentity = resolveIdentityColumn(pTableSchema, tmpColumnNames);
74
+
75
+ // (Deleted, ID) composite — equality on Deleted then range on the identity.
76
+ if (tmpColumnNames.has('Deleted') && tmpColumnNames.has(tmpIdentity))
77
+ {
78
+ tmpIndices.push(
79
+ {
80
+ Name: `${INDEX_POLICY_PREFIX}${tmpTableName}_Deleted_${tmpIdentity}`,
81
+ TableName: tmpTableName,
82
+ Columns: [ 'Deleted', tmpIdentity ],
83
+ Unique: false,
84
+ Strategy: ''
85
+ });
86
+ }
87
+
88
+ // GUID lookup — NON-UNIQUE by policy (see file header).
89
+ let tmpGUIDColumn = tmpColumns.find((pColumn) => { return pColumn.DataType === 'GUID'; });
90
+ if (tmpGUIDColumn)
91
+ {
92
+ tmpIndices.push(
93
+ {
94
+ Name: `${INDEX_POLICY_PREFIX}${tmpTableName}_${tmpGUIDColumn.Column}`,
95
+ TableName: tmpTableName,
96
+ Columns: [ tmpGUIDColumn.Column ],
97
+ Unique: false,
98
+ Strategy: ''
99
+ });
100
+ }
101
+ }
102
+
103
+ // -- 2. Caller-declared per-table extras --
104
+ let tmpTableExtras = (tmpConfig.TableIndexes && Array.isArray(tmpConfig.TableIndexes[tmpTableName]))
105
+ ? tmpConfig.TableIndexes[tmpTableName]
106
+ : [];
107
+ for (let i = 0; i < tmpTableExtras.length; i++)
108
+ {
109
+ let tmpExtra = tmpTableExtras[i];
110
+ let tmpCols = Array.isArray(tmpExtra.Columns) ? tmpExtra.Columns : [ tmpExtra.Columns ];
111
+ // Skip a declared index whose columns are not all present on the table.
112
+ if (!tmpCols.every((pCol) => { return tmpColumnNames.has(pCol); }))
113
+ {
114
+ continue;
115
+ }
116
+ tmpIndices.push(
117
+ {
118
+ Name: tmpExtra.Name || `${INDEX_POLICY_PREFIX}${tmpTableName}_${tmpCols.join('_')}`,
119
+ TableName: tmpTableName,
120
+ Columns: tmpCols,
121
+ Unique: !!tmpExtra.Unique,
122
+ Strategy: tmpExtra.Strategy || ''
123
+ });
124
+ }
125
+
126
+ return tmpIndices;
127
+ }
128
+
129
+ /**
130
+ * Whether an index name is one this policy manages — used by convergence's
131
+ * "managed" prune scope so it only ever drops indexes it owns. Recognizes:
132
+ * - policy-created indexes (IX_M_SYNC_ prefix),
133
+ * - meadow schema-managed indexes (AK_M / IX_M prefixes),
134
+ * - the legacy precursor / MeadowConnectionManager artifacts, which name a
135
+ * single-column index after the column itself (e.g. [Deleted], [GUIDDocument]).
136
+ * A truly external, hand-authored index (unusual here, but possible) is NOT
137
+ * managed and is left untouched.
138
+ *
139
+ * @param {string} pIndexName
140
+ * @param {object} pTableSchema
141
+ * @returns {boolean}
142
+ */
143
+ function isManagedIndexName(pIndexName, pTableSchema)
144
+ {
145
+ if (!pIndexName)
146
+ {
147
+ return false;
148
+ }
149
+ if (pIndexName.indexOf(INDEX_POLICY_PREFIX) === 0
150
+ || pIndexName.indexOf('AK_M') === 0
151
+ || pIndexName.indexOf('IX_M') === 0)
152
+ {
153
+ return true;
154
+ }
155
+ // Legacy artifact: index named exactly after one of the table's columns.
156
+ let tmpColumns = Array.isArray(pTableSchema && pTableSchema.Columns) ? pTableSchema.Columns : [];
157
+ return tmpColumns.some((pColumn) => { return pColumn.Column === pIndexName; });
158
+ }
159
+
160
+ module.exports = {
161
+ INDEX_POLICY_PREFIX: INDEX_POLICY_PREFIX,
162
+ resolveIdentityColumn: resolveIdentityColumn,
163
+ resolveDesiredIndexes: resolveDesiredIndexes,
164
+ isManagedIndexName: isManagedIndexName
165
+ };
@@ -1,5 +1,6 @@
1
1
  const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
2
  const libMeadowOperation = require('./Meadow-Service-Operation.js');
3
+ const libSyncPoolLimit = require('./Meadow-Service-Sync-PoolLimit.js');
3
4
 
4
5
  class MeadowSyncEntityInitial extends libFableServiceProviderBase
5
6
  {
@@ -48,6 +49,12 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
48
49
  // override the automatic Deleted=0 filter (e.g. "includeDeleted=true").
49
50
  this.SyncDeletedRecordsQueryString = this.options.SyncDeletedRecordsQueryString || '';
50
51
 
52
+ // How many records to upsert concurrently within a table. Defaults to the
53
+ // DB connection pool size (capped) so fan-out never oversubscribes the pool
54
+ // nor runs unbounded on a very large pool. Overridable globally or
55
+ // per-table via SyncEntityOptions.SyncRecordConcurrency.
56
+ this.SyncRecordConcurrency = this._resolveSyncRecordConcurrency(this.options.SyncRecordConcurrency);
57
+
51
58
  this.Meadow = false;
52
59
 
53
60
  this.operation = new libMeadowOperation(this.fable);
@@ -55,6 +62,27 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
55
62
  this.skipSync = false;
56
63
  }
57
64
 
65
+ /**
66
+ * Resolve the per-record fan-out concurrency for this entity.
67
+ *
68
+ * Precedence: an explicit configured value (global or per-table) wins and is
69
+ * used as-is; otherwise fall back to the automatic default (pool size, capped)
70
+ * — see resolveDefaultConcurrency in Meadow-Service-Sync-PoolLimit.js.
71
+ *
72
+ * @param {number|string} pConfigured - configured concurrency, if any
73
+ * @returns {number} a positive integer concurrency
74
+ */
75
+ _resolveSyncRecordConcurrency(pConfigured)
76
+ {
77
+ let tmpConfigured = parseInt(pConfigured, 10);
78
+ if (!isNaN(tmpConfigured) && tmpConfigured > 0)
79
+ {
80
+ return tmpConfigured;
81
+ }
82
+
83
+ return libSyncPoolLimit.resolveDefaultConcurrency(this.fable);
84
+ }
85
+
58
86
  initialize(fCallback)
59
87
  {
60
88
  if (this.fable.hasOwnProperty('Meadow'))
@@ -80,66 +108,18 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
80
108
 
81
109
  return tmpProvider.createTable(this.EntitySchema, (pCreateError) =>
82
110
  {
83
-
84
111
  if (pCreateError)
85
112
  {
86
113
  this.log.warn(`${this.EntitySchema.TableName}: createTable returned error: ${pCreateError}`);
87
114
  }
88
115
 
89
- // Sync-entity init intentionally does not create indexes here.
90
- // In the DataCloner flow there is no MeadowConnectionManager
91
- // service registered, so the legacy createIndex path was dead
92
- // code that just logged noise ("No connection manager
93
- // available; skipping index creation for X") for every table.
94
- // Indexes are created via the provider's own createIndices /
95
- // generateCreateIndexStatements methods, invoked by the
96
- // DataCloner's dedicated /indices/create endpoint after the
97
- // initial sync is complete — that's the point where it makes
98
- // sense to build indexes, since they're expensive on populated
99
- // tables.
100
- //
101
- // Preserve the legacy MeadowConnectionManager path for the CLI
102
- // Meadow-Integration-Command-DataClone.js flow (which does
103
- // register that service), but do it silently when neither
104
- // indexable column nor connection manager is available.
105
- const tmpGUIDColumn = this.EntitySchema.Columns.find((c) => c.DataType == 'GUID');
106
- const tmpDeletedColumn = this.EntitySchema.Columns.find((c) => c.Column == 'Deleted');
107
-
108
- let tmpCanCreateIndexes = (tmpGUIDColumn || tmpDeletedColumn)
109
- && this.fable.MeadowConnectionManager
110
- && this.fable.MeadowConnectionManager.ConnectionPool
111
- && typeof(this.fable.MeadowConnectionManager.createIndex) === 'function';
112
-
113
- if (!tmpCanCreateIndexes)
114
- {
115
- return fCallback(pCreateError);
116
- }
117
-
118
- let tmpAnticipate = this.fable.newAnticipate();
119
- if (tmpGUIDColumn)
120
- {
121
- tmpAnticipate.anticipate(
122
- (fNext) =>
123
- {
124
- return this.fable.MeadowConnectionManager.createIndex(this.EntitySchema, tmpGUIDColumn, true, fNext);
125
- });
126
- }
127
- if (tmpDeletedColumn)
128
- {
129
- tmpAnticipate.anticipate(
130
- (fNext) =>
131
- {
132
- return this.fable.MeadowConnectionManager.createIndex(this.EntitySchema, tmpDeletedColumn, false, fNext);
133
- });
134
- }
135
- tmpAnticipate.wait((pIndexError) =>
136
- {
137
- if (pIndexError)
138
- {
139
- this.log.warn(`${this.EntitySchema.TableName}: Index creation error: ${pIndexError}`);
140
- }
141
- return fCallback(pIndexError || pCreateError);
142
- });
116
+ // Init only ensures the table exists. Index provisioning is
117
+ // handled separately and authoritatively by the index-convergence
118
+ // step (Meadow-Service-IndexConvergence), so indexes have a single
119
+ // writer the legacy per-column MeadowConnectionManager.createIndex
120
+ // path was retired to avoid two mechanisms fighting over the same
121
+ // indexes.
122
+ return fCallback(pCreateError);
143
123
  });
144
124
  }
145
125
 
@@ -249,7 +229,7 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
249
229
  return fPageComplete();
250
230
  }
251
231
 
252
- this.fable.Utility.eachLimit(pBody, 5,
232
+ this.fable.Utility.eachLimit(pBody, this.SyncRecordConcurrency,
253
233
  (pEntityRecord, fRecordComplete) =>
254
234
  {
255
235
  const tmpRecordID = pEntityRecord[this.DefaultIdentifier];
@@ -545,7 +525,7 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
545
525
  // Shared record-processing function used by both pagination modes
546
526
  const fProcessPageRecords = (pBody, fPageProcessComplete) =>
547
527
  {
548
- this.fable.Utility.eachLimit(pBody, 5,
528
+ this.fable.Utility.eachLimit(pBody, this.SyncRecordConcurrency,
549
529
  (pEntityRecord, fRawEntitySyncComplete) =>
550
530
  {
551
531
  // node:sqlite resolves its callbacks synchronously, so the