foxhound 2.0.25 → 2.0.27

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.
Files changed (37) hide show
  1. package/README.md +22 -22
  2. package/docs/README.md +24 -24
  3. package/docs/_cover.md +1 -1
  4. package/docs/_version.json +7 -0
  5. package/docs/api/behaviorFlags.md +1 -1
  6. package/docs/api/buildQuery.md +2 -2
  7. package/docs/api/clone.md +1 -1
  8. package/docs/api/setScope.md +1 -1
  9. package/docs/architecture.md +4 -4
  10. package/docs/css/docuserve.css +277 -23
  11. package/docs/dialects/README.md +4 -4
  12. package/docs/dialects/postgresql.md +1 -1
  13. package/docs/dialects/sqlite.md +5 -5
  14. package/docs/index.html +2 -2
  15. package/docs/joins.md +3 -3
  16. package/docs/query/README.md +4 -4
  17. package/docs/query/create.md +4 -4
  18. package/docs/query/update.md +10 -10
  19. package/docs/query-overrides.md +1 -1
  20. package/docs/quickstart.md +5 -5
  21. package/docs/retold-catalog.json +1 -1
  22. package/docs/retold-keyword-index.json +1 -1
  23. package/docs/schema.md +32 -32
  24. package/docs/sorting.md +4 -4
  25. package/package.json +3 -2
  26. package/source/dialects/ALASQL/FoxHound-Dialect-ALASQL.js +10 -8
  27. package/source/dialects/DGraph/FoxHound-Dialect-DGraph.js +8 -6
  28. package/source/dialects/MicrosoftSQL/FoxHound-Dialect-MSSQL.js +120 -0
  29. package/source/dialects/MongoDB/FoxHound-Dialect-MongoDB.js +8 -6
  30. package/source/dialects/MySQL/FoxHound-Dialect-MySQL.js +11 -8
  31. package/source/dialects/PostgreSQL/FoxHound-Dialect-PostgreSQL.js +10 -6
  32. package/source/dialects/SQLite/FoxHound-Dialect-SQLite.js +11 -8
  33. package/source/dialects/Solr/FoxHound-Dialect-Solr.js +8 -6
  34. package/test/FoxHound-Dialect-ALASQL_tests.js +3 -1
  35. package/test/FoxHound-Dialect-MySQL_tests.js +3 -1
  36. package/test/FoxHound-Dialect-SQLite_tests.js +3 -1
  37. package/test/Foxhound-Dialect-MSSQL_tests.js +59 -4
@@ -8,9 +8,9 @@ FoxHound builds queries through a two-phase process: **configure** then **build*
8
8
  Configure ──► Build ──► Access Results
9
9
  ```
10
10
 
11
- 1. **Configure** set the scope, fields, filters, sorts, joins, pagination, records, and dialect
12
- 2. **Build** call one of the `build*Query()` methods to generate the SQL
13
- 3. **Access** read `query.body` for the SQL string and `query.parameters` for bound values
11
+ 1. **Configure** -- set the scope, fields, filters, sorts, joins, pagination, records, and dialect
12
+ 2. **Build** -- call one of the `build*Query()` methods to generate the SQL
13
+ 3. **Access** -- read `query.body` for the SQL string and `query.parameters` for bound values
14
14
 
15
15
  ## Creating a Query Instance
16
16
 
@@ -84,7 +84,7 @@ After a query is built, you can reset the parameters for reuse or clone the quer
84
84
  // Reset to default parameters
85
85
  tmpQuery.resetParameters();
86
86
 
87
- // Clone copies scope, begin, cap, schema, filters, sorts, and dataElements
87
+ // Clone -- copies scope, begin, cap, schema, filters, sorts, and dataElements
88
88
  var tmpClone = tmpQuery.clone();
89
89
  ```
90
90
 
@@ -60,7 +60,7 @@ tmpQuery.setDisableDeleteTracking(true); // Include delete columns on insert
60
60
 
61
61
  The INSERT syntax is largely the same across dialects, with a few differences:
62
62
 
63
- - **MySQL** uses backtick-quoted identifiers and `:name` parameters
64
- - **MSSQL** uses bracket-quoted identifiers, `@name` parameters, and skips the AutoIdentity column entirely (rather than inserting NULL)
65
- - **SQLite** uses backtick-quoted identifiers and `:name` parameters
66
- - **ALASQL** same as SQLite
63
+ - **MySQL** -- uses backtick-quoted identifiers and `:name` parameters
64
+ - **MSSQL** -- uses bracket-quoted identifiers, `@name` parameters, and skips the AutoIdentity column entirely (rather than inserting NULL)
65
+ - **SQLite** -- uses backtick-quoted identifiers and `:name` parameters
66
+ - **ALASQL** -- same as SQLite
@@ -27,13 +27,13 @@ When a schema is present, FoxHound manages certain columns automatically:
27
27
 
28
28
  | Schema Type | Behavior on Update |
29
29
  |------------|-------------------|
30
- | `AutoIdentity` | **Skipped** never updated |
31
- | `CreateDate` | **Skipped** set only on insert |
32
- | `CreateIDUser` | **Skipped** set only on insert |
30
+ | `AutoIdentity` | **Skipped** -- never updated |
31
+ | `CreateDate` | **Skipped** -- set only on insert |
32
+ | `CreateIDUser` | **Skipped** -- set only on insert |
33
33
  | `UpdateDate` | Set to current timestamp automatically |
34
34
  | `UpdateIDUser` | Set to the value from `setIDUser()` |
35
- | `DeleteDate` | **Skipped** managed by delete operations |
36
- | `DeleteIDUser` | **Skipped** managed by delete operations |
35
+ | `DeleteDate` | **Skipped** -- managed by delete operations |
36
+ | `DeleteIDUser` | **Skipped** -- managed by delete operations |
37
37
 
38
38
  ## Disabling Auto-Management
39
39
 
@@ -44,13 +44,13 @@ tmpQuery.setDisableAutoUserStamp(true); // Don't auto-set UpdateIDUser
44
44
 
45
45
  ## Important Notes
46
46
 
47
- - The record passed to `addRecord()` should contain only the columns you want to change FoxHound generates SET clauses for each key in the record object
47
+ - The record passed to `addRecord()` should contain only the columns you want to change -- FoxHound generates SET clauses for each key in the record object
48
48
  - Always include a filter (usually on the primary key) to avoid updating all rows
49
49
  - If the record object is empty or no records have been added, `buildUpdateQuery()` returns `false` for the query body
50
50
 
51
51
  ## Dialect Differences
52
52
 
53
- - **MySQL** backtick-quoted identifiers, `:name` parameters
54
- - **MSSQL** bracket-quoted identifiers, `@name` parameters; special handling for `UpdateDate` with `disableAutoDateStamp`
55
- - **SQLite** backtick-quoted identifiers, `:name` parameters
56
- - **ALASQL** same as SQLite
53
+ - **MySQL** -- backtick-quoted identifiers, `:name` parameters
54
+ - **MSSQL** -- bracket-quoted identifiers, `@name` parameters; special handling for `UpdateDate` with `disableAutoDateStamp`
55
+ - **SQLite** -- backtick-quoted identifiers, `:name` parameters
56
+ - **ALASQL** -- same as SQLite
@@ -85,4 +85,4 @@ Query overrides are useful when you need:
85
85
  - `UNION` queries
86
86
  - Any SQL feature not directly supported by FoxHound's fluent API
87
87
 
88
- For straightforward CRUD operations, the standard query builders are preferred they are safer and more portable across dialects.
88
+ For straightforward CRUD operations, the standard query builders are preferred -- they are safer and more portable across dialects.
@@ -189,8 +189,8 @@ console.log(tmpQuery.query.body);
189
189
 
190
190
  ## Next Steps
191
191
 
192
- - [Architecture](architecture.md) understand FoxHound's internal design
193
- - [Filters](filters.md) learn about filter operators and grouping
194
- - [Schema Integration](schema.md) use schemas for automatic column management
195
- - [Dialects](dialects/README.md) explore dialect-specific features
196
- - [API Reference](api/README.md) complete function reference
192
+ - [Architecture](architecture.md) -- understand FoxHound's internal design
193
+ - [Filters](filters.md) -- learn about filter operators and grouping
194
+ - [Schema Integration](schema.md) -- use schemas for automatic column management
195
+ - [Dialects](dialects/README.md) -- explore dialect-specific features
196
+ - [API Reference](api/README.md) -- complete function reference
@@ -1,5 +1,5 @@
1
1
  {
2
- "Generated": "2026-03-03T14:33:09.873Z",
2
+ "Generated": "2026-04-10T17:20:13.644Z",
3
3
  "GitHubOrg": "stevenvelozo",
4
4
  "DefaultBranch": "master",
5
5
  "Groups": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "Generated": "2026-03-03T14:33:09.982Z",
2
+ "Generated": "2026-04-10T17:20:13.902Z",
3
3
  "DocumentCount": 27,
4
4
  "LunrIndex": {
5
5
  "version": "2.3.9",
package/docs/schema.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Schema Integration
2
2
 
3
- FoxHound is schema-aware when a schema array is attached to a query, it uses the column type annotations to automatically manage identity columns, timestamps, user stamps, and soft-delete tracking.
3
+ FoxHound is schema-aware -- when a schema array is attached to a query, it uses the column type annotations to automatically manage identity columns, timestamps, user stamps, and soft-delete tracking.
4
4
 
5
5
  ## Attaching a Schema
6
6
 
@@ -29,22 +29,22 @@ tmpQuery.query.schema = [
29
29
 
30
30
  | Type | Purpose | Create | Read | Update | Delete | Undelete |
31
31
  |------|---------|--------|------|--------|--------|----------|
32
- | `AutoIdentity` | Auto-increment primary key | `NULL` (DB assigns) | included | **skipped** | | |
33
- | `AutoGUID` | Auto-generated UUID | UUID or user value | included | included | | |
34
- | `CreateDate` | Row creation timestamp | `NOW()` | included | **skipped** | | |
35
- | `CreateIDUser` | Row creator user ID | `IDUser` | included | **skipped** | | |
32
+ | `AutoIdentity` | Auto-increment primary key | `NULL` (DB assigns) | included | **skipped** | -- | -- |
33
+ | `AutoGUID` | Auto-generated UUID | UUID or user value | included | included | -- | -- |
34
+ | `CreateDate` | Row creation timestamp | `NOW()` | included | **skipped** | -- | -- |
35
+ | `CreateIDUser` | Row creator user ID | `IDUser` | included | **skipped** | -- | -- |
36
36
  | `UpdateDate` | Last modification timestamp | `NOW()` | included | `NOW()` | `NOW()` | `NOW()` |
37
- | `UpdateIDUser` | Last modifier user ID | `IDUser` | included | `IDUser` | | `IDUser` |
38
- | `Deleted` | Soft-delete flag | `0` | auto-filtered | | set to `1` | set to `0` |
39
- | `DeleteDate` | Deletion timestamp | **skipped** | included | **skipped** | `NOW()` | |
40
- | `DeleteIDUser` | Deleter user ID | **skipped** | included | **skipped** | `IDUser` | |
41
- | `String` | Text data | parameterized | included | parameterized | | |
42
- | `Integer` | Numeric data | parameterized | included | parameterized | | |
43
- | `Decimal` | Decimal data | parameterized | included | parameterized | | |
44
- | `Boolean` | Boolean data | parameterized | included | parameterized | | |
45
- | `DateTime` | Date/time data | parameterized | included | parameterized | | |
46
- | `JSON` | Structured JSON data | `JSON.stringify` | included | `JSON.stringify` | | |
47
- | `JSONProxy` | JSON with different SQL column name | `JSON.stringify` to `StorageColumn` | included | `JSON.stringify` to `StorageColumn` | | |
37
+ | `UpdateIDUser` | Last modifier user ID | `IDUser` | included | `IDUser` | -- | `IDUser` |
38
+ | `Deleted` | Soft-delete flag | `0` | auto-filtered | -- | set to `1` | set to `0` |
39
+ | `DeleteDate` | Deletion timestamp | **skipped** | included | **skipped** | `NOW()` | -- |
40
+ | `DeleteIDUser` | Deleter user ID | **skipped** | included | **skipped** | `IDUser` | -- |
41
+ | `String` | Text data | parameterized | included | parameterized | -- | -- |
42
+ | `Integer` | Numeric data | parameterized | included | parameterized | -- | -- |
43
+ | `Decimal` | Decimal data | parameterized | included | parameterized | -- | -- |
44
+ | `Boolean` | Boolean data | parameterized | included | parameterized | -- | -- |
45
+ | `DateTime` | Date/time data | parameterized | included | parameterized | -- | -- |
46
+ | `JSON` | Structured JSON data | `JSON.stringify` | included | `JSON.stringify` | -- | -- |
47
+ | `JSONProxy` | JSON with different SQL column name | `JSON.stringify` to `StorageColumn` | included | `JSON.stringify` to `StorageColumn` | -- | -- |
48
48
 
49
49
  ## JSON and JSON Proxy Types
50
50
 
@@ -99,33 +99,33 @@ Nested paths are supported (e.g., `Metadata.dimensions.width`). JSON Proxy colum
99
99
 
100
100
  ### Create (INSERT)
101
101
 
102
- - `AutoIdentity` inserts `NULL` (MySQL/SQLite) or is omitted (MSSQL)
103
- - `AutoGUID` generates a UUID via Fable, unless the record has a valid GUID already
104
- - `CreateDate`, `UpdateDate` inserts the current timestamp
105
- - `CreateIDUser`, `UpdateIDUser` inserts the user ID from `setIDUser()`
106
- - `DeleteDate`, `DeleteIDUser` **skipped** (when delete tracking is enabled)
102
+ - `AutoIdentity` -> inserts `NULL` (MySQL/SQLite) or is omitted (MSSQL)
103
+ - `AutoGUID` -> generates a UUID via Fable, unless the record has a valid GUID already
104
+ - `CreateDate`, `UpdateDate` -> inserts the current timestamp
105
+ - `CreateIDUser`, `UpdateIDUser` -> inserts the user ID from `setIDUser()`
106
+ - `DeleteDate`, `DeleteIDUser` -> **skipped** (when delete tracking is enabled)
107
107
 
108
108
  ### Update
109
109
 
110
- - `AutoIdentity`, `CreateDate`, `CreateIDUser`, `DeleteDate`, `DeleteIDUser` **skipped**
111
- - `UpdateDate` set to current timestamp automatically
112
- - `UpdateIDUser` set to the value from `setIDUser()`
113
- - All other columns parameterized from the record
110
+ - `AutoIdentity`, `CreateDate`, `CreateIDUser`, `DeleteDate`, `DeleteIDUser` -> **skipped**
111
+ - `UpdateDate` -> set to current timestamp automatically
112
+ - `UpdateIDUser` -> set to the value from `setIDUser()`
113
+ - All other columns -> parameterized from the record
114
114
 
115
115
  ### Delete (Soft)
116
116
 
117
117
  Only these columns are modified:
118
- - `Deleted` set to `1`
119
- - `DeleteDate` set to current timestamp
120
- - `UpdateDate` set to current timestamp
121
- - `DeleteIDUser` set to the value from `setIDUser()`
118
+ - `Deleted` -> set to `1`
119
+ - `DeleteDate` -> set to current timestamp
120
+ - `UpdateDate` -> set to current timestamp
121
+ - `DeleteIDUser` -> set to the value from `setIDUser()`
122
122
 
123
123
  ### Undelete
124
124
 
125
125
  Only these columns are modified:
126
- - `Deleted` set to `0`
127
- - `UpdateDate` set to current timestamp
128
- - `UpdateIDUser` set to the value from `setIDUser()`
126
+ - `Deleted` -> set to `0`
127
+ - `UpdateDate` -> set to current timestamp
128
+ - `UpdateIDUser` -> set to the value from `setIDUser()`
129
129
 
130
130
  ### Read / Count
131
131
 
package/docs/sorting.md CHANGED
@@ -33,7 +33,7 @@ tmpQuery
33
33
  // ORDER BY Genre, PublishedYear DESC
34
34
  ```
35
35
 
36
- Columns without an explicit `Direction` (or with `Direction: 'Ascending'`) sort in ascending order the SQL default.
36
+ Columns without an explicit `Direction` (or with `Direction: 'Ascending'`) sort in ascending order -- the SQL default.
37
37
 
38
38
  ## Setting Sorts Directly
39
39
 
@@ -71,9 +71,9 @@ tmpQuery.setSort({Column: 'Title', Direction: 'Descending'});
71
71
 
72
72
  The `ORDER BY` clause syntax is consistent across all SQL dialects. The main difference is in identifier quoting:
73
73
 
74
- - **MySQL** `ORDER BY PublishedYear DESC`
75
- - **MSSQL** `ORDER BY [PublishedYear] DESC`
76
- - **SQLite/ALASQL** `ORDER BY \`PublishedYear\` DESC`
74
+ - **MySQL** -- `ORDER BY PublishedYear DESC`
75
+ - **MSSQL** -- `ORDER BY [PublishedYear] DESC`
76
+ - **SQLite/ALASQL** -- `ORDER BY \`PublishedYear\` DESC`
77
77
 
78
78
  ## Interaction with Pagination
79
79
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foxhound",
3
- "version": "2.0.25",
3
+ "version": "2.0.27",
4
4
  "description": "A Database Query generation library.",
5
5
  "main": "source/FoxHound.js",
6
6
  "scripts": {
@@ -48,7 +48,8 @@
48
48
  },
49
49
  "homepage": "https://github.com/stevenvelozo/foxhound",
50
50
  "devDependencies": {
51
- "quackage": "^1.0.63"
51
+ "pict-docuserve": "^0.1.5",
52
+ "quackage": "^1.1.0"
52
53
  },
53
54
  "dependencies": {
54
55
  "fable": "^3.1.63",
@@ -352,12 +352,6 @@ var FoxHoundDialectALASQL = function(pFable)
352
352
  }
353
353
  }
354
354
 
355
- if (pParameters.query.disableAutoDateStamp &&
356
- tmpSchemaEntry.Type === 'UpdateDate')
357
- {
358
- // This is ignored if flag is set
359
- continue;
360
- }
361
355
  if (pParameters.query.disableAutoUserStamp &&
362
356
  tmpSchemaEntry.Type === 'UpdateIDUser')
363
357
  {
@@ -382,8 +376,16 @@ var FoxHoundDialectALASQL = function(pFable)
382
376
  switch (tmpSchemaEntry.Type)
383
377
  {
384
378
  case 'UpdateDate':
385
- // This is an autoidentity, so we don't parameterize it and just pass in NULL
386
- tmpUpdate += ' '+escapeColumn(tmpColumn, pParameters)+' = NOW()';
379
+ if (pParameters.query.disableAutoDateStamp)
380
+ {
381
+ var tmpColumnParameter = tmpColumn+'_'+tmpCurrentColumn;
382
+ tmpUpdate += ' '+escapeColumn(tmpColumn, pParameters)+' = :'+tmpColumnParameter;
383
+ pParameters.query.parameters[tmpColumnParameter] = tmpRecords[0][tmpColumn];
384
+ }
385
+ else
386
+ {
387
+ tmpUpdate += ' '+escapeColumn(tmpColumn, pParameters)+' = NOW()';
388
+ }
387
389
  break;
388
390
  case 'UpdateIDUser':
389
391
  // This is the user ID, which we hope is in the query.
@@ -489,11 +489,6 @@ var FoxHoundDialectDGraph = function(pFable)
489
489
  {
490
490
  var tmpSchemaEntry = findSchemaEntry(tmpColumn, tmpSchema);
491
491
 
492
- if (pParameters.query.disableAutoDateStamp &&
493
- tmpSchemaEntry.Type === 'UpdateDate')
494
- {
495
- continue;
496
- }
497
492
  if (pParameters.query.disableAutoUserStamp &&
498
493
  tmpSchemaEntry.Type === 'UpdateIDUser')
499
494
  {
@@ -513,7 +508,14 @@ var FoxHoundDialectDGraph = function(pFable)
513
508
  switch (tmpSchemaEntry.Type)
514
509
  {
515
510
  case 'UpdateDate':
516
- tmpUpdateDoc[tmpColumn] = '$$NOW';
511
+ if (pParameters.query.disableAutoDateStamp)
512
+ {
513
+ tmpUpdateDoc[tmpColumn] = tmpRecords[0][tmpColumn];
514
+ }
515
+ else
516
+ {
517
+ tmpUpdateDoc[tmpColumn] = '$$NOW';
518
+ }
517
519
  break;
518
520
  case 'UpdateIDUser':
519
521
  tmpUpdateDoc[tmpColumn] = pParameters.query.IDUser;
@@ -179,6 +179,51 @@ var FoxHoundDialectMSSQL = function(pFable)
179
179
  return tmpFieldList;
180
180
  };
181
181
 
182
+ /**
183
+ * Generate a field list for the outer SELECT of the legacy pagination
184
+ * wrapper. The outer FROM is a subquery aliased as [_Paged], so the
185
+ * default "[Table].*" qualifier can't resolve there — we need either
186
+ * an explicit column list from the schema or a bare "*".
187
+ *
188
+ * If the caller set explicit dataElements, reuse them (they reference
189
+ * bare column names, which work fine against the subquery alias).
190
+ * Otherwise emit an explicit list from the schema to keep [_RowNum]
191
+ * from leaking. As a last resort, fall back to "*" — callers without
192
+ * a schema will see [_RowNum] as an extra property on marshalled
193
+ * records but the query itself remains valid.
194
+ *
195
+ * @param: {Object} pParameters SQL Query Parameters
196
+ * @return: {String} Field list (prefixed with a single leading space)
197
+ */
198
+ var generateOuterFieldListForLegacyPagination = function(pParameters)
199
+ {
200
+ var tmpDataElements = pParameters.dataElements;
201
+ if (Array.isArray(tmpDataElements) && tmpDataElements.length > 0)
202
+ {
203
+ // Reuse the caller-supplied list. It emits unqualified column
204
+ // names ([Col], [Col] AS [Alias]) which resolve fine against
205
+ // the subquery alias.
206
+ return generateFieldList(pParameters);
207
+ }
208
+
209
+ var tmpSchema = Array.isArray(pParameters.query.schema) ? pParameters.query.schema : [];
210
+ if (tmpSchema.length > 0)
211
+ {
212
+ var tmpList = ' ';
213
+ for (var i = 0; i < tmpSchema.length; i++)
214
+ {
215
+ if (i > 0) tmpList += ', ';
216
+ tmpList += generateSafeFieldName(tmpSchema[i].Column);
217
+ }
218
+ return tmpList;
219
+ }
220
+
221
+ // No schema, no explicit dataElements — "*" is the best we can do.
222
+ // [_RowNum] will surface on marshalled records; downstream code can
223
+ // ignore it. Schemas are the norm via Meadow so this is rare.
224
+ return ' *';
225
+ };
226
+
182
227
  /**
183
228
  * Ensure a field name is properly escaped.
184
229
  */
@@ -352,12 +397,41 @@ var FoxHoundDialectMSSQL = function(pFable)
352
397
  return tmpWhere;
353
398
  };
354
399
 
400
+ /**
401
+ * Find the table's AutoIdentity primary-key column from the schema, if any.
402
+ * Used as a deterministic default ORDER BY when the caller didn't set a
403
+ * sort — MSSQL pagination (both OFFSET/FETCH and ROW_NUMBER) requires an
404
+ * ORDER BY clause or it produces a syntax error.
405
+ *
406
+ * @param: {Object} pParameters SQL Query Parameters
407
+ * @return: {String|null} The column name, or null if none found
408
+ */
409
+ var findPrimaryKeyColumn = function(pParameters)
410
+ {
411
+ var tmpSchema = Array.isArray(pParameters.query.schema) ? pParameters.query.schema : [];
412
+ for (var i = 0; i < tmpSchema.length; i++)
413
+ {
414
+ if (tmpSchema[i].Type === 'AutoIdentity')
415
+ {
416
+ return tmpSchema[i].Column;
417
+ }
418
+ }
419
+ return null;
420
+ };
421
+
355
422
  /**
356
423
  * Generate an ORDER BY clause from the sort array
357
424
  *
358
425
  * Each entry in the sort is an object like:
359
426
  * {Column:'Color',Direction:'Descending'}
360
427
  *
428
+ * When no sort is specified but the query has a cap (pagination is
429
+ * active), inject a default ORDER BY on the primary key so MSSQL
430
+ * doesn't reject the OFFSET/FETCH or ROW_NUMBER clause. Without a
431
+ * schema the PK can't be inferred — fall back to `ORDER BY (SELECT 1)`
432
+ * which is legal for OFFSET/FETCH but not for ROW_NUMBER (the legacy
433
+ * pagination path handles that case by refusing to paginate).
434
+ *
361
435
  * @method: generateOrderBy
362
436
  * @param: {Object} pParameters SQL Query Parameters
363
437
  * @return: {String} Returns the field list clause
@@ -367,6 +441,15 @@ var FoxHoundDialectMSSQL = function(pFable)
367
441
  var tmpOrderBy = pParameters.sort;
368
442
  if (!Array.isArray(tmpOrderBy) || tmpOrderBy.length < 1)
369
443
  {
444
+ if (pParameters.cap)
445
+ {
446
+ var tmpPK = findPrimaryKeyColumn(pParameters);
447
+ if (tmpPK)
448
+ {
449
+ return ' ORDER BY ['+tmpPK+']';
450
+ }
451
+ return ' ORDER BY (SELECT 1)';
452
+ }
370
453
  return '';
371
454
  }
372
455
 
@@ -390,6 +473,12 @@ var FoxHoundDialectMSSQL = function(pFable)
390
473
  /**
391
474
  * Generate the limit clause
392
475
  *
476
+ * When `legacyPagination` is set on pParameters the limit is emitted
477
+ * by the Read function using a ROW_NUMBER() subquery wrapper instead
478
+ * (OFFSET/FETCH NEXT requires SQL Server 2012+ / compatibility level
479
+ * 110+, which some customers don't have). In that case this function
480
+ * returns an empty string.
481
+ *
393
482
  * @method: generateLimit
394
483
  * @param: {Object} pParameters SQL Query Parameters
395
484
  * @return: {String} Returns the table limit clause
@@ -401,6 +490,13 @@ var FoxHoundDialectMSSQL = function(pFable)
401
490
  return '';
402
491
  }
403
492
 
493
+ if (pParameters.legacyPagination)
494
+ {
495
+ // The Read function wraps the query in a ROW_NUMBER() subquery
496
+ // instead of appending an OFFSET/FETCH tail clause.
497
+ return '';
498
+ }
499
+
404
500
  var tmpLimit = ' OFFSET ';
405
501
  // If there is a begin record, we'll pass that in as well.
406
502
  if (pParameters.begin !== false)
@@ -1037,6 +1133,30 @@ var FoxHoundDialectMSSQL = function(pFable)
1037
1133
  }
1038
1134
  }
1039
1135
 
1136
+ // Legacy pagination path — emit a ROW_NUMBER() wrapper instead of
1137
+ // OFFSET/FETCH. Required for SQL Server 2008 R2 and earlier, or
1138
+ // for databases running at a compatibility level below 110 (2012).
1139
+ // Enabled via pParameters.legacyPagination (forwarded from the
1140
+ // meadow-connection-mssql provider's LegacyPagination config).
1141
+ if (pParameters.legacyPagination && pParameters.cap)
1142
+ {
1143
+ var tmpBegin = (pParameters.begin !== false) ? pParameters.begin : 0;
1144
+ var tmpEnd = tmpBegin + pParameters.cap;
1145
+ // generateOrderBy always returns a usable ORDER BY when cap is
1146
+ // set. ROW_NUMBER()'s OVER() clause takes the same body but
1147
+ // without the leading space.
1148
+ var tmpOverClause = tmpOrderBy.replace(/^ /, '');
1149
+ // The outer SELECT's FROM is the subquery alias, not the base
1150
+ // table — so the default field list's "[Table].*" qualifier
1151
+ // won't resolve at the outer level. Compute an outer field
1152
+ // list that works regardless of whether the caller supplied
1153
+ // explicit dataElements or relied on the default.
1154
+ var tmpOuterFieldList = generateOuterFieldListForLegacyPagination(pParameters);
1155
+ // INDEX hints and JOINs live on the inner select (they apply
1156
+ // to the base table). [_RowNum] is confined to the subquery.
1157
+ return `SELECT${tmpOptDistinct}${tmpOuterFieldList} FROM (SELECT${tmpFieldList}, ROW_NUMBER() OVER (${tmpOverClause}) AS [_RowNum] FROM${tmpTableName}${tmpIndexHints}${tmpJoin}${tmpWhere}) AS [_Paged] WHERE [_RowNum] > ${tmpBegin} AND [_RowNum] <= ${tmpEnd};`;
1158
+ }
1159
+
1040
1160
  return `SELECT${tmpOptDistinct}${tmpFieldList} FROM${tmpTableName}${tmpIndexHints}${tmpJoin}${tmpWhere}${tmpOrderBy}${tmpLimit};`;
1041
1161
  };
1042
1162
 
@@ -492,11 +492,6 @@ var FoxHoundDialectMongoDB = function(pFable)
492
492
  {
493
493
  var tmpSchemaEntry = findSchemaEntry(tmpColumn, tmpSchema);
494
494
 
495
- if (pParameters.query.disableAutoDateStamp &&
496
- tmpSchemaEntry.Type === 'UpdateDate')
497
- {
498
- continue;
499
- }
500
495
  if (pParameters.query.disableAutoUserStamp &&
501
496
  tmpSchemaEntry.Type === 'UpdateIDUser')
502
497
  {
@@ -516,7 +511,14 @@ var FoxHoundDialectMongoDB = function(pFable)
516
511
  switch (tmpSchemaEntry.Type)
517
512
  {
518
513
  case 'UpdateDate':
519
- tmpUpdateDoc[tmpColumn] = '$$NOW';
514
+ if (pParameters.query.disableAutoDateStamp)
515
+ {
516
+ tmpUpdateDoc[tmpColumn] = tmpRecords[0][tmpColumn];
517
+ }
518
+ else
519
+ {
520
+ tmpUpdateDoc[tmpColumn] = '$$NOW';
521
+ }
520
522
  break;
521
523
  case 'UpdateIDUser':
522
524
  tmpUpdateDoc[tmpColumn] = pParameters.query.IDUser;
@@ -434,12 +434,6 @@ var FoxHoundDialectMySQL = function(pFable)
434
434
  }
435
435
  }
436
436
 
437
- if (pParameters.query.disableAutoDateStamp &&
438
- tmpSchemaEntry.Type === 'UpdateDate')
439
- {
440
- // This is ignored if flag is set
441
- continue;
442
- }
443
437
  if (pParameters.query.disableAutoUserStamp &&
444
438
  tmpSchemaEntry.Type === 'UpdateIDUser')
445
439
  {
@@ -464,8 +458,17 @@ var FoxHoundDialectMySQL = function(pFable)
464
458
  switch (tmpSchemaEntry.Type)
465
459
  {
466
460
  case 'UpdateDate':
467
- // This is an autoidentity, so we don't parameterize it and just pass in NULL
468
- tmpUpdate += ' '+tmpColumn+' = ' + SQL_NOW;
461
+ if (pParameters.query.disableAutoDateStamp)
462
+ {
463
+ // Manual mode: use the record's value as-is
464
+ var tmpColumnParameter = tmpColumn+'_'+tmpCurrentColumn;
465
+ tmpUpdate += ' '+tmpColumn+' = :'+tmpColumnParameter;
466
+ pParameters.query.parameters[tmpColumnParameter] = tmpRecords[0][tmpColumn];
467
+ }
468
+ else
469
+ {
470
+ tmpUpdate += ' '+tmpColumn+' = ' + SQL_NOW;
471
+ }
469
472
  break;
470
473
  case 'UpdateIDUser':
471
474
  // This is the user ID, which we hope is in the query.
@@ -398,11 +398,6 @@ var FoxHoundDialectPostgreSQL = function(pFable)
398
398
  }
399
399
  }
400
400
 
401
- if (pParameters.query.disableAutoDateStamp &&
402
- tmpSchemaEntry.Type === 'UpdateDate')
403
- {
404
- continue;
405
- }
406
401
  if (pParameters.query.disableAutoUserStamp &&
407
402
  tmpSchemaEntry.Type === 'UpdateIDUser')
408
403
  {
@@ -425,7 +420,16 @@ var FoxHoundDialectPostgreSQL = function(pFable)
425
420
  switch (tmpSchemaEntry.Type)
426
421
  {
427
422
  case 'UpdateDate':
428
- tmpUpdate += ' '+generateSafeFieldName(tmpColumn)+' = ' + SQL_NOW;
423
+ if (pParameters.query.disableAutoDateStamp)
424
+ {
425
+ var tmpColumnParameter = tmpColumn+'_'+tmpCurrentColumn;
426
+ tmpUpdate += ' '+generateSafeFieldName(tmpColumn)+' = :'+tmpColumnParameter;
427
+ pParameters.query.parameters[tmpColumnParameter] = tmpRecords[0][tmpColumn];
428
+ }
429
+ else
430
+ {
431
+ tmpUpdate += ' '+generateSafeFieldName(tmpColumn)+' = ' + SQL_NOW;
432
+ }
429
433
  break;
430
434
  case 'UpdateIDUser':
431
435
  var tmpColumnParameter = tmpColumn+'_'+tmpCurrentColumn;
@@ -380,12 +380,6 @@ var FoxHoundDialectSQLite = function(pFable)
380
380
  }
381
381
  }
382
382
 
383
- if (pParameters.query.disableAutoDateStamp &&
384
- tmpSchemaEntry.Type === 'UpdateDate')
385
- {
386
- // This is ignored if flag is set
387
- continue;
388
- }
389
383
  if (pParameters.query.disableAutoUserStamp &&
390
384
  tmpSchemaEntry.Type === 'UpdateIDUser')
391
385
  {
@@ -410,8 +404,17 @@ var FoxHoundDialectSQLite = function(pFable)
410
404
  switch (tmpSchemaEntry.Type)
411
405
  {
412
406
  case 'UpdateDate':
413
- // This is an autoidentity, so we don't parameterize it and just pass in NULL
414
- tmpUpdate += ' '+escapeColumn(tmpColumn, pParameters)+' = NOW()';
407
+ if (pParameters.query.disableAutoDateStamp)
408
+ {
409
+ // Manual mode: use the record's value as-is
410
+ var tmpColumnParameter = tmpColumn+'_'+tmpCurrentColumn;
411
+ tmpUpdate += ' '+escapeColumn(tmpColumn, pParameters)+' = :'+tmpColumnParameter;
412
+ pParameters.query.parameters[tmpColumnParameter] = tmpRecords[0][tmpColumn];
413
+ }
414
+ else
415
+ {
416
+ tmpUpdate += ' '+escapeColumn(tmpColumn, pParameters)+' = NOW()';
417
+ }
415
418
  break;
416
419
  case 'UpdateIDUser':
417
420
  // This is the user ID, which we hope is in the query.
@@ -461,11 +461,6 @@ var FoxHoundDialectSolr = function(pFable)
461
461
  {
462
462
  var tmpSchemaEntry = findSchemaEntry(tmpColumn, tmpSchema);
463
463
 
464
- if (pParameters.query.disableAutoDateStamp &&
465
- tmpSchemaEntry.Type === 'UpdateDate')
466
- {
467
- continue;
468
- }
469
464
  if (pParameters.query.disableAutoUserStamp &&
470
465
  tmpSchemaEntry.Type === 'UpdateIDUser')
471
466
  {
@@ -485,7 +480,14 @@ var FoxHoundDialectSolr = function(pFable)
485
480
  switch (tmpSchemaEntry.Type)
486
481
  {
487
482
  case 'UpdateDate':
488
- tmpUpdateDoc[tmpColumn] = { 'set': '$$NOW' };
483
+ if (pParameters.query.disableAutoDateStamp)
484
+ {
485
+ tmpUpdateDoc[tmpColumn] = { 'set': tmpRecords[0][tmpColumn] };
486
+ }
487
+ else
488
+ {
489
+ tmpUpdateDoc[tmpColumn] = { 'set': '$$NOW' };
490
+ }
489
491
  break;
490
492
  case 'UpdateIDUser':
491
493
  tmpUpdateDoc[tmpColumn] = { 'set': pParameters.query.IDUser };
@@ -792,8 +792,10 @@ suite
792
792
  tmpQuery.buildUpdateQuery();
793
793
  // This is the query generated by the ALASQL dialect
794
794
  _Fable.log.trace('Update Query', tmpQuery.query);
795
+ // When disableAutoDateStamp is true, UpdateDate is included with the
796
+ // record's value instead of being auto-stamped with NOW()
795
797
  Expect(tmpQuery.query.body)
796
- .to.equal('UPDATE Animal SET `GUIDAnimal` = :GUIDAnimal_0, `Name` = :Name_1, `Age` = :Age_2 WHERE `IDAnimal` = :IDAnimal_w0;');
798
+ .to.equal('UPDATE Animal SET `GUIDAnimal` = :GUIDAnimal_0, `UpdateDate` = :UpdateDate_1, `Name` = :Name_2, `Age` = :Age_3 WHERE `IDAnimal` = :IDAnimal_w0;');
797
799
  }
798
800
  );
799
801
  test
@@ -752,8 +752,10 @@ suite
752
752
  tmpQuery.buildUpdateQuery();
753
753
  // This is the query generated by the MySQL dialect
754
754
  _Fable.log.trace('Update Query', tmpQuery.query);
755
+ // When disableAutoDateStamp is true, UpdateDate is included with the
756
+ // record's value instead of being auto-stamped with NOW()
755
757
  Expect(tmpQuery.query.body)
756
- .to.equal('UPDATE `Animal` SET GUIDAnimal = :GUIDAnimal_0, Name = :Name_1, Age = :Age_2 WHERE IDAnimal = :IDAnimal_w0;');
758
+ .to.equal('UPDATE `Animal` SET GUIDAnimal = :GUIDAnimal_0, UpdateDate = :UpdateDate_1, Name = :Name_2, Age = :Age_3 WHERE IDAnimal = :IDAnimal_w0;');
757
759
  }
758
760
  );
759
761
  test