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.
- package/README.md +22 -22
- package/docs/README.md +24 -24
- package/docs/_cover.md +1 -1
- package/docs/_version.json +7 -0
- package/docs/api/behaviorFlags.md +1 -1
- package/docs/api/buildQuery.md +2 -2
- package/docs/api/clone.md +1 -1
- package/docs/api/setScope.md +1 -1
- package/docs/architecture.md +4 -4
- package/docs/css/docuserve.css +277 -23
- package/docs/dialects/README.md +4 -4
- package/docs/dialects/postgresql.md +1 -1
- package/docs/dialects/sqlite.md +5 -5
- package/docs/index.html +2 -2
- package/docs/joins.md +3 -3
- package/docs/query/README.md +4 -4
- package/docs/query/create.md +4 -4
- package/docs/query/update.md +10 -10
- package/docs/query-overrides.md +1 -1
- package/docs/quickstart.md +5 -5
- package/docs/retold-catalog.json +1 -1
- package/docs/retold-keyword-index.json +1 -1
- package/docs/schema.md +32 -32
- package/docs/sorting.md +4 -4
- package/package.json +3 -2
- package/source/dialects/ALASQL/FoxHound-Dialect-ALASQL.js +10 -8
- package/source/dialects/DGraph/FoxHound-Dialect-DGraph.js +8 -6
- package/source/dialects/MicrosoftSQL/FoxHound-Dialect-MSSQL.js +120 -0
- package/source/dialects/MongoDB/FoxHound-Dialect-MongoDB.js +8 -6
- package/source/dialects/MySQL/FoxHound-Dialect-MySQL.js +11 -8
- package/source/dialects/PostgreSQL/FoxHound-Dialect-PostgreSQL.js +10 -6
- package/source/dialects/SQLite/FoxHound-Dialect-SQLite.js +11 -8
- package/source/dialects/Solr/FoxHound-Dialect-Solr.js +8 -6
- package/test/FoxHound-Dialect-ALASQL_tests.js +3 -1
- package/test/FoxHound-Dialect-MySQL_tests.js +3 -1
- package/test/FoxHound-Dialect-SQLite_tests.js +3 -1
- package/test/Foxhound-Dialect-MSSQL_tests.js +59 -4
package/docs/query/README.md
CHANGED
|
@@ -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**
|
|
12
|
-
2. **Build**
|
|
13
|
-
3. **Access**
|
|
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
|
|
87
|
+
// Clone -- copies scope, begin, cap, schema, filters, sorts, and dataElements
|
|
88
88
|
var tmpClone = tmpQuery.clone();
|
|
89
89
|
```
|
|
90
90
|
|
package/docs/query/create.md
CHANGED
|
@@ -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**
|
|
64
|
-
- **MSSQL**
|
|
65
|
-
- **SQLite**
|
|
66
|
-
- **ALASQL**
|
|
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
|
package/docs/query/update.md
CHANGED
|
@@ -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**
|
|
31
|
-
| `CreateDate` | **Skipped**
|
|
32
|
-
| `CreateIDUser` | **Skipped**
|
|
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**
|
|
36
|
-
| `DeleteIDUser` | **Skipped**
|
|
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
|
|
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**
|
|
54
|
-
- **MSSQL**
|
|
55
|
-
- **SQLite**
|
|
56
|
-
- **ALASQL**
|
|
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
|
package/docs/query-overrides.md
CHANGED
|
@@ -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
|
|
88
|
+
For straightforward CRUD operations, the standard query builders are preferred -- they are safer and more portable across dialects.
|
package/docs/quickstart.md
CHANGED
|
@@ -189,8 +189,8 @@ console.log(tmpQuery.query.body);
|
|
|
189
189
|
|
|
190
190
|
## Next Steps
|
|
191
191
|
|
|
192
|
-
- [Architecture](architecture.md)
|
|
193
|
-
- [Filters](filters.md)
|
|
194
|
-
- [Schema Integration](schema.md)
|
|
195
|
-
- [Dialects](dialects/README.md)
|
|
196
|
-
- [API Reference](api/README.md)
|
|
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
|
package/docs/retold-catalog.json
CHANGED
package/docs/schema.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Schema Integration
|
|
2
2
|
|
|
3
|
-
FoxHound is schema-aware
|
|
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` |
|
|
38
|
-
| `Deleted` | Soft-delete flag | `0` | auto-filtered |
|
|
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`
|
|
103
|
-
- `AutoGUID`
|
|
104
|
-
- `CreateDate`, `UpdateDate`
|
|
105
|
-
- `CreateIDUser`, `UpdateIDUser`
|
|
106
|
-
- `DeleteDate`, `DeleteIDUser`
|
|
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`
|
|
111
|
-
- `UpdateDate`
|
|
112
|
-
- `UpdateIDUser`
|
|
113
|
-
- All other columns
|
|
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`
|
|
119
|
-
- `DeleteDate`
|
|
120
|
-
- `UpdateDate`
|
|
121
|
-
- `DeleteIDUser`
|
|
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`
|
|
127
|
-
- `UpdateDate`
|
|
128
|
-
- `UpdateIDUser`
|
|
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
|
|
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**
|
|
75
|
-
- **MSSQL**
|
|
76
|
-
- **SQLite/ALASQL**
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
|
|
386
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
468
|
-
|
|
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
|
-
|
|
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
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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` = :
|
|
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 = :
|
|
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
|