foxhound 2.0.23 → 2.0.25

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 CHANGED
@@ -89,6 +89,8 @@ When a schema is attached, FoxHound automatically manages special columns:
89
89
  | `UpdateDate` / `UpdateIDUser` | Auto-populated on insert and update |
90
90
  | `DeleteDate` / `DeleteIDUser` | Auto-populated on soft delete |
91
91
  | `Deleted` | Soft-delete flag — auto-filtered in reads |
92
+ | `JSON` | Structured JSON data — serialized to `TEXT` on write, parsed on read |
93
+ | `JSONProxy` | JSON stored in a different SQL column — uses `StorageColumn` for SQL, virtual name for objects |
92
94
 
93
95
  ## Filter Operators
94
96
 
package/docs/_sidebar.md CHANGED
@@ -50,5 +50,6 @@
50
50
  - Advanced
51
51
 
52
52
  - [Schema Integration](schema.md)
53
+ - [JSON Columns](json-columns.md)
53
54
  - [Query Overrides](query-overrides.md)
54
55
  - [Configuration Reference](configuration.md)
package/docs/filters.md CHANGED
@@ -166,6 +166,34 @@ The operator codes are:
166
166
  | `LE` | `<=` |
167
167
  | `LK` | `LIKE` |
168
168
 
169
+ ## JSON Path Filtering
170
+
171
+ When a schema is attached and contains `JSON` or `JSONProxy` columns, you can filter on nested JSON properties using dot notation:
172
+
173
+ ```javascript
174
+ // Filter where Metadata.habitat equals 'forest'
175
+ tmpQuery.addFilter('Metadata.habitat', 'forest');
176
+
177
+ // Filter where Metadata.weight is greater than 100
178
+ tmpQuery.addFilter('Metadata.weight', 100, '>');
179
+
180
+ // Nested paths work too
181
+ tmpQuery.addFilter('Metadata.dimensions.height', 50, '>=');
182
+ ```
183
+
184
+ FoxHound detects the dot notation, resolves the base column against the schema, and generates the appropriate JSON path expression for the active dialect:
185
+
186
+ | Dialect | Single-Level | Nested |
187
+ |---------|-------------|--------|
188
+ | MySQL | `JSON_EXTRACT(col, '$.key')` | `JSON_EXTRACT(col, '$.key1.key2')` |
189
+ | PostgreSQL | `col->>'key'` | `col#>>'{key1,key2}'` |
190
+ | SQLite | `json_extract(col, '$.key')` | `json_extract(col, '$.key1.key2')` |
191
+ | MSSQL | `JSON_VALUE(col, '$.key')` | `JSON_VALUE(col, '$.key1.key2')` |
192
+
193
+ For `JSONProxy` columns, the storage column name is used in the SQL expression automatically. For example, if `Preferences` is a `JSONProxy` with `StorageColumn: 'PreferencesJSON'`, filtering on `Preferences.theme` produces `json_extract(PreferencesJSON, '$.theme')` in SQLite.
194
+
195
+ All standard comparison operators (`=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`) work with JSON path filters.
196
+
169
197
  ## Soft-Delete Auto-Filter
170
198
 
171
199
  When a schema with a `Deleted` column type is present and delete tracking is not disabled, FoxHound automatically appends a `WHERE Deleted = 0` filter to all Read and Count queries. If you explicitly add a filter on the `Deleted` column, the automatic filter is suppressed.
@@ -0,0 +1,145 @@
1
+ # JSON Column Support
2
+
3
+ > Automatic serialization and JSON path filtering for structured data columns
4
+
5
+ FoxHound provides schema-aware handling of JSON data types. When a schema with `JSON` or `JSONProxy` columns is attached, FoxHound automatically serializes object values on write and generates dialect-specific JSON path expressions for filtering.
6
+
7
+ ## Schema Types
8
+
9
+ ### JSON
10
+
11
+ The SQL column and JavaScript property share the same name.
12
+
13
+ ```javascript
14
+ { Column: 'Metadata', Type: 'JSON' }
15
+ ```
16
+
17
+ ### JSONProxy
18
+
19
+ The SQL column differs from the JavaScript property. The `StorageColumn` specifies the actual SQL column.
20
+
21
+ ```javascript
22
+ { Column: 'Preferences', Type: 'JSONProxy', StorageColumn: 'PreferencesJSON' }
23
+ ```
24
+
25
+ ## Write Operations (Create / Update)
26
+
27
+ On CREATE and UPDATE, FoxHound automatically calls `JSON.stringify` on JSON column values:
28
+
29
+ ```javascript
30
+ tmpQuery.query.schema = [
31
+ { Column: 'IDProduct', Type: 'AutoIdentity' },
32
+ { Column: 'Name', Type: 'String' },
33
+ { Column: 'Metadata', Type: 'JSON' },
34
+ { Column: 'Preferences', Type: 'JSONProxy', StorageColumn: 'PreferencesJSON' }
35
+ ];
36
+
37
+ tmpQuery.addRecord({
38
+ Name: 'Widget',
39
+ Metadata: { color: 'blue' },
40
+ Preferences: { theme: 'dark' }
41
+ });
42
+ tmpQuery.setDialect('MySQL').buildCreateQuery();
43
+ ```
44
+
45
+ Generated SQL:
46
+
47
+ ```sql
48
+ INSERT INTO Product (Name, Metadata, PreferencesJSON)
49
+ VALUES (:Name_0, :Metadata_1, :Preferences_2);
50
+ ```
51
+
52
+ Parameters:
53
+
54
+ ```javascript
55
+ {
56
+ Name_0: 'Widget',
57
+ Metadata_1: '{"color":"blue"}', // JSON.stringify'd
58
+ Preferences_2: '{"theme":"dark"}' // JSON.stringify'd, stored in PreferencesJSON
59
+ }
60
+ ```
61
+
62
+ Key behaviors:
63
+ - **JSON**: Column name in SQL matches the property name. Value is serialized.
64
+ - **JSONProxy**: `StorageColumn` is used as the SQL column name. Value is serialized from the virtual property.
65
+ - If a value is already a string, it is passed through without double-serialization.
66
+
67
+ ## JSON Path Filtering
68
+
69
+ FoxHound supports filtering on nested JSON properties using dot notation in column names. When a filter column contains a dot and the base name matches a JSON or JSONProxy schema entry, FoxHound generates a JSON path expression.
70
+
71
+ ### Usage
72
+
73
+ ```javascript
74
+ tmpQuery
75
+ .addFilter('Metadata.color', 'blue')
76
+ .addFilter('Metadata.weight', 100, '>')
77
+ .addFilter('Metadata.dimensions.height', 50, '>=');
78
+ ```
79
+
80
+ ### Dialect Output
81
+
82
+ #### MySQL
83
+
84
+ ```sql
85
+ WHERE JSON_EXTRACT(`Metadata`, '$.color') = :Metadata_color_w0
86
+ AND JSON_EXTRACT(`Metadata`, '$.weight') > :Metadata_weight_w1
87
+ AND JSON_EXTRACT(`Metadata`, '$.dimensions.height') >= :Metadata_dimensions_height_w2
88
+ ```
89
+
90
+ #### PostgreSQL
91
+
92
+ Single-level paths use the `->>` operator; nested paths use `#>>`:
93
+
94
+ ```sql
95
+ WHERE "Metadata"->>'color' = :Metadata_color_w0
96
+ AND "Metadata"->>'weight' > :Metadata_weight_w1
97
+ AND "Metadata"#>>'{dimensions,height}' >= :Metadata_dimensions_height_w2
98
+ ```
99
+
100
+ #### SQLite
101
+
102
+ ```sql
103
+ WHERE json_extract(`Metadata`, '$.color') = :Metadata_color_w0
104
+ AND json_extract(`Metadata`, '$.weight') > :Metadata_weight_w1
105
+ AND json_extract(`Metadata`, '$.dimensions.height') >= :Metadata_dimensions_height_w2
106
+ ```
107
+
108
+ #### MSSQL
109
+
110
+ ```sql
111
+ WHERE JSON_VALUE([Metadata], '$.color') = @Metadata_color_w0
112
+ AND JSON_VALUE([Metadata], '$.weight') > @Metadata_weight_w1
113
+ AND JSON_VALUE([Metadata], '$.dimensions.height') >= @Metadata_dimensions_height_w2
114
+ ```
115
+
116
+ ### JSONProxy Resolution
117
+
118
+ For `JSONProxy` columns, the storage column name is automatically used in the generated SQL. Filtering on `Preferences.theme` when `Preferences` has `StorageColumn: 'PreferencesJSON'`:
119
+
120
+ ```sql
121
+ -- MySQL
122
+ WHERE JSON_EXTRACT(`PreferencesJSON`, '$.theme') = :Preferences_theme_w0
123
+
124
+ -- PostgreSQL
125
+ WHERE "PreferencesJSON"->>'theme' = :Preferences_theme_w0
126
+ ```
127
+
128
+ ### Supported Operators
129
+
130
+ All standard filter operators work with JSON path expressions: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`.
131
+
132
+ ### ALASQL Limitation
133
+
134
+ ALASQL does not support JSON path functions. JSON columns work for basic CRUD (values are serialized/deserialized), but JSON path filtering is not available.
135
+
136
+ ## Database Requirements
137
+
138
+ JSON path filtering requires these minimum database versions:
139
+
140
+ | Database | Minimum Version | Function Used |
141
+ |----------|----------------|---------------|
142
+ | MySQL | 5.7 | `JSON_EXTRACT` |
143
+ | PostgreSQL | 9.3 | `->>` / `#>>` |
144
+ | SQLite | 3.38 | `json_extract` |
145
+ | SQL Server | 2016 | `JSON_VALUE` |
package/docs/schema.md CHANGED
@@ -13,6 +13,8 @@ tmpQuery.query.schema = [
13
13
  {Column: 'Title', Type: 'String'},
14
14
  {Column: 'Author', Type: 'String'},
15
15
  {Column: 'PublishedYear', Type: 'Integer'},
16
+ {Column: 'Metadata', Type: 'JSON'},
17
+ {Column: 'Extras', Type: 'JSONProxy', StorageColumn: 'ExtrasJSON'},
16
18
  {Column: 'CreateDate', Type: 'CreateDate'},
17
19
  {Column: 'CreatingIDUser', Type: 'CreateIDUser'},
18
20
  {Column: 'UpdateDate', Type: 'UpdateDate'},
@@ -41,6 +43,57 @@ tmpQuery.query.schema = [
41
43
  | `Decimal` | Decimal data | parameterized | included | parameterized | — | — |
42
44
  | `Boolean` | Boolean data | parameterized | included | parameterized | — | — |
43
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
+
49
+ ## JSON and JSON Proxy Types
50
+
51
+ FoxHound supports two schema types for structured JSON data stored as `TEXT` in SQL databases.
52
+
53
+ ### JSON
54
+
55
+ The `JSON` type marks a column whose value should be serialized with `JSON.stringify` on write and deserialized with `JSON.parse` on read. The SQL column name matches the object property name.
56
+
57
+ ```javascript
58
+ { Column: 'Metadata', Type: 'JSON' }
59
+ ```
60
+
61
+ On CREATE and UPDATE, FoxHound automatically calls `JSON.stringify` on the value. If the value is already a string, it is passed through as-is.
62
+
63
+ ### JSON Proxy
64
+
65
+ The `JSONProxy` type stores JSON in a SQL column with a different name than the JavaScript property. The `StorageColumn` property specifies the actual SQL column name.
66
+
67
+ ```javascript
68
+ { Column: 'Preferences', Type: 'JSONProxy', StorageColumn: 'PreferencesJSON' }
69
+ ```
70
+
71
+ On CREATE and UPDATE, FoxHound:
72
+ - Uses `StorageColumn` (`PreferencesJSON`) as the column name in the SQL statement
73
+ - Calls `JSON.stringify` on the value from the `Column` property (`Preferences`)
74
+
75
+ On READ, the Meadow provider layer handles deserialization: the raw `PreferencesJSON` text column is parsed and mapped to the `Preferences` property, and the storage column is hidden from the result object.
76
+
77
+ ### JSON Path Filtering
78
+
79
+ You can filter on nested JSON properties using dot notation in `addFilter`:
80
+
81
+ ```javascript
82
+ tmpQuery
83
+ .addFilter('Metadata.habitat', 'forest')
84
+ .addFilter('Metadata.weight', 100, '>');
85
+ ```
86
+
87
+ FoxHound generates dialect-specific JSON path expressions:
88
+
89
+ | Dialect | Generated SQL |
90
+ |---------|---------------|
91
+ | MySQL | `JSON_EXTRACT(Metadata, '$.habitat') = :Metadata_habitat_w0` |
92
+ | PostgreSQL | `Metadata->>'habitat' = :Metadata_habitat_w0` |
93
+ | SQLite | `json_extract(Metadata, '$.habitat') = :Metadata_habitat_w0` |
94
+ | MSSQL | `JSON_VALUE(Metadata, '$.habitat') = :Metadata_habitat_w0` |
95
+
96
+ Nested paths are supported (e.g., `Metadata.dimensions.width`). JSON Proxy columns are automatically resolved to their storage column in the SQL expression.
44
97
 
45
98
  ## How Schema Affects Each Operation
46
99
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foxhound",
3
- "version": "2.0.23",
3
+ "version": "2.0.25",
4
4
  "description": "A Database Query generation library.",
5
5
  "main": "source/FoxHound.js",
6
6
  "scripts": {
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "homepage": "https://github.com/stevenvelozo/foxhound",
50
50
  "devDependencies": {
51
- "quackage": "^1.0.60"
51
+ "quackage": "^1.0.63"
52
52
  },
53
53
  "dependencies": {
54
54
  "fable": "^3.1.63",
@@ -112,6 +112,27 @@ var FoxHoundDialectALASQL = function(pFable)
112
112
  return tmpFieldList;
113
113
  };
114
114
 
115
+ var resolveJsonColumnPath = function(pColumnName, pSchema)
116
+ {
117
+ if (!Array.isArray(pSchema) || pSchema.length < 1) return null;
118
+ var tmpParts = pColumnName.replace(/`/g, '').replace(/"/g, '').split('.');
119
+ for (var tmpStartIdx = 0; tmpStartIdx < Math.min(tmpParts.length - 1, 2); tmpStartIdx++)
120
+ {
121
+ var tmpBaseColumn = tmpParts[tmpStartIdx];
122
+ for (var s = 0; s < pSchema.length; s++)
123
+ {
124
+ if (pSchema[s].Column === tmpBaseColumn &&
125
+ (pSchema[s].Type === 'JSON' || pSchema[s].Type === 'JSONProxy'))
126
+ {
127
+ var tmpActualColumn = (pSchema[s].Type === 'JSONProxy') ? pSchema[s].StorageColumn : tmpBaseColumn;
128
+ var tmpJsonPath = '$.' + tmpParts.slice(tmpStartIdx + 1).join('.');
129
+ return { column: tmpActualColumn, path: tmpJsonPath };
130
+ }
131
+ }
132
+ }
133
+ return null;
134
+ };
135
+
115
136
  /**
116
137
  * Generate a query from the array of where clauses
117
138
  *
@@ -372,6 +393,20 @@ var FoxHoundDialectALASQL = function(pFable)
372
393
  // Set the query parameter
373
394
  pParameters.query.parameters[tmpColumnParameter] = pParameters.query.IDUser;
374
395
  break;
396
+ case 'JSON':
397
+ var tmpJSONUpdateParam = tmpColumn+'_'+tmpCurrentColumn;
398
+ tmpUpdate += ' '+tmpColumn+' = :'+tmpJSONUpdateParam;
399
+ pParameters.query.parameters[tmpJSONUpdateParam] = (typeof tmpRecords[0][tmpColumn] === 'string')
400
+ ? tmpRecords[0][tmpColumn]
401
+ : JSON.stringify(tmpRecords[0][tmpColumn] || {});
402
+ break;
403
+ case 'JSONProxy':
404
+ var tmpProxyUpdateParam = tmpSchemaEntry.StorageColumn+'_'+tmpCurrentColumn;
405
+ tmpUpdate += ' '+tmpSchemaEntry.StorageColumn+' = :'+tmpProxyUpdateParam;
406
+ pParameters.query.parameters[tmpProxyUpdateParam] = (typeof tmpRecords[0][tmpColumn] === 'string')
407
+ ? tmpRecords[0][tmpColumn]
408
+ : JSON.stringify(tmpRecords[0][tmpColumn] || {});
409
+ break;
375
410
  default:
376
411
  var tmpColumnDefaultParameter = tmpColumn+'_'+tmpCurrentColumn;
377
412
  tmpUpdate += ' '+escapeColumn(tmpColumn, pParameters)+' = :'+tmpColumnDefaultParameter;
@@ -667,6 +702,20 @@ var FoxHoundDialectALASQL = function(pFable)
667
702
  pParameters.query.parameters[tmpColumnParameter] = pParameters.query.IDUser;
668
703
  }
669
704
  break;
705
+ case 'JSON':
706
+ var tmpJSONCreateParam = tmpColumn+'_'+tmpCurrentColumn;
707
+ tmpCreateSet += ' :'+tmpJSONCreateParam;
708
+ pParameters.query.parameters[tmpJSONCreateParam] = (typeof tmpRecords[0][tmpColumn] === 'string')
709
+ ? tmpRecords[0][tmpColumn]
710
+ : JSON.stringify(tmpRecords[0][tmpColumn] || {});
711
+ break;
712
+ case 'JSONProxy':
713
+ var tmpProxyCreateParam = tmpColumn+'_'+tmpCurrentColumn;
714
+ tmpCreateSet += ' :'+tmpProxyCreateParam;
715
+ pParameters.query.parameters[tmpProxyCreateParam] = (typeof tmpRecords[0][tmpColumn] === 'string')
716
+ ? tmpRecords[0][tmpColumn]
717
+ : JSON.stringify(tmpRecords[0][tmpColumn] || {});
718
+ break;
670
719
  default:
671
720
  buildDefaultDefinition();
672
721
  break;
@@ -727,6 +776,20 @@ var FoxHoundDialectALASQL = function(pFable)
727
776
  }
728
777
  switch (tmpSchemaEntry.Type)
729
778
  {
779
+ case 'JSON':
780
+ if (tmpCreateSet != '')
781
+ {
782
+ tmpCreateSet += ',';
783
+ }
784
+ tmpCreateSet += ' '+tmpColumn;
785
+ break;
786
+ case 'JSONProxy':
787
+ if (tmpCreateSet != '')
788
+ {
789
+ tmpCreateSet += ',';
790
+ }
791
+ tmpCreateSet += ' '+tmpSchemaEntry.StorageColumn;
792
+ break;
730
793
  default:
731
794
  if (tmpCreateSet != '')
732
795
  {
@@ -195,6 +195,27 @@ var FoxHoundDialectMSSQL = function(pFable)
195
195
  }
196
196
  }
197
197
 
198
+ var resolveJsonColumnPath = function(pColumnName, pSchema)
199
+ {
200
+ if (!Array.isArray(pSchema) || pSchema.length < 1) return null;
201
+ var tmpParts = pColumnName.replace(/`/g, '').replace(/"/g, '').split('.');
202
+ for (var tmpStartIdx = 0; tmpStartIdx < Math.min(tmpParts.length - 1, 2); tmpStartIdx++)
203
+ {
204
+ var tmpBaseColumn = tmpParts[tmpStartIdx];
205
+ for (var s = 0; s < pSchema.length; s++)
206
+ {
207
+ if (pSchema[s].Column === tmpBaseColumn &&
208
+ (pSchema[s].Type === 'JSON' || pSchema[s].Type === 'JSONProxy'))
209
+ {
210
+ var tmpActualColumn = (pSchema[s].Type === 'JSONProxy') ? pSchema[s].StorageColumn : tmpBaseColumn;
211
+ var tmpJsonPath = '$.' + tmpParts.slice(tmpStartIdx + 1).join('.');
212
+ return { column: tmpActualColumn, path: tmpJsonPath };
213
+ }
214
+ }
215
+ }
216
+ return null;
217
+ };
218
+
198
219
  /**
199
220
  * Generate a query from the array of where clauses
200
221
  *
@@ -313,8 +334,16 @@ var FoxHoundDialectMSSQL = function(pFable)
313
334
  else
314
335
  {
315
336
  tmpColumnParameter = tmpFilter[i].Parameter+'_w'+i;
316
- // Add the column name, operator and parameter name to the list of where value parenthetical
317
- tmpWhere += ' ['+tmpFilter[i].Column+'] '+tmpFilter[i].Operator+' @'+tmpColumnParameter;
337
+ var tmpSchema = Array.isArray(pParameters.query.schema) ? pParameters.query.schema : [];
338
+ var tmpJsonRef = resolveJsonColumnPath(tmpFilter[i].Column, tmpSchema);
339
+ if (tmpJsonRef)
340
+ {
341
+ tmpWhere += ' JSON_VALUE(['+tmpJsonRef.column+"], '"+tmpJsonRef.path+"') "+tmpFilter[i].Operator+' :'+tmpColumnParameter;
342
+ }
343
+ else
344
+ {
345
+ tmpWhere += ' ['+tmpFilter[i].Column+'] '+tmpFilter[i].Operator+' @'+tmpColumnParameter;
346
+ }
318
347
  pParameters.query.parameters[tmpColumnParameter] = tmpFilter[i].Value;
319
348
  generateMSSQLParameterTypeEntry(pParameters, tmpColumnParameter, tmpFilter[i].Parameter)
320
349
  }
@@ -523,6 +552,22 @@ var FoxHoundDialectMSSQL = function(pFable)
523
552
  pParameters.query.parameters[tmpColumnParameter] = pParameters.query.IDUser;
524
553
  generateMSSQLParameterTypeEntry(pParameters, tmpColumnParameter, tmpColumn)
525
554
  break;
555
+ case 'JSON':
556
+ var tmpJSONUpdateParam = tmpColumn+'_'+tmpCurrentColumn;
557
+ tmpUpdate += ' ['+tmpColumn+'] = @'+tmpJSONUpdateParam;
558
+ pParameters.query.parameters[tmpJSONUpdateParam] = (typeof tmpRecords[0][tmpColumn] === 'string')
559
+ ? tmpRecords[0][tmpColumn]
560
+ : JSON.stringify(tmpRecords[0][tmpColumn] || {});
561
+ generateMSSQLParameterTypeEntry(pParameters, tmpJSONUpdateParam, {Type:'String'});
562
+ break;
563
+ case 'JSONProxy':
564
+ var tmpProxyUpdateParam = tmpSchemaEntry.StorageColumn+'_'+tmpCurrentColumn;
565
+ tmpUpdate += ' ['+tmpSchemaEntry.StorageColumn+'] = @'+tmpProxyUpdateParam;
566
+ pParameters.query.parameters[tmpProxyUpdateParam] = (typeof tmpRecords[0][tmpColumn] === 'string')
567
+ ? tmpRecords[0][tmpColumn]
568
+ : JSON.stringify(tmpRecords[0][tmpColumn] || {});
569
+ generateMSSQLParameterTypeEntry(pParameters, tmpProxyUpdateParam, {Type:'String'});
570
+ break;
526
571
  default:
527
572
  var tmpColumnDefaultParameter = tmpColumn+'_'+tmpCurrentColumn;
528
573
  tmpUpdate += ' ['+tmpColumn+'] = @'+tmpColumnDefaultParameter;
@@ -822,6 +867,22 @@ var FoxHoundDialectMSSQL = function(pFable)
822
867
  generateMSSQLParameterTypeEntry(pParameters, tmpColumnParameter, tmpSchemaEntry)
823
868
  }
824
869
  break;
870
+ case 'JSON':
871
+ var tmpJSONCreateParam = tmpColumn+'_'+tmpCurrentColumn;
872
+ tmpCreateSet += ' @'+tmpJSONCreateParam;
873
+ pParameters.query.parameters[tmpJSONCreateParam] = (typeof tmpRecords[0][tmpColumn] === 'string')
874
+ ? tmpRecords[0][tmpColumn]
875
+ : JSON.stringify(tmpRecords[0][tmpColumn] || {});
876
+ generateMSSQLParameterTypeEntry(pParameters, tmpJSONCreateParam, {Type:'String'});
877
+ break;
878
+ case 'JSONProxy':
879
+ var tmpProxyCreateParam = tmpColumn+'_'+tmpCurrentColumn;
880
+ tmpCreateSet += ' @'+tmpProxyCreateParam;
881
+ pParameters.query.parameters[tmpProxyCreateParam] = (typeof tmpRecords[0][tmpColumn] === 'string')
882
+ ? tmpRecords[0][tmpColumn]
883
+ : JSON.stringify(tmpRecords[0][tmpColumn] || {});
884
+ generateMSSQLParameterTypeEntry(pParameters, tmpProxyCreateParam, {Type:'String'});
885
+ break;
825
886
  default:
826
887
  buildDefaultDefinition();
827
888
  break;
@@ -893,6 +954,20 @@ var FoxHoundDialectMSSQL = function(pFable)
893
954
  tmpCreateSet += ' ['+tmpColumn+']';
894
955
  }
895
956
  continue;
957
+ case 'JSON':
958
+ if (tmpCreateSet != '')
959
+ {
960
+ tmpCreateSet += ',';
961
+ }
962
+ tmpCreateSet += ' ['+tmpColumn+']';
963
+ break;
964
+ case 'JSONProxy':
965
+ if (tmpCreateSet != '')
966
+ {
967
+ tmpCreateSet += ',';
968
+ }
969
+ tmpCreateSet += ' ['+tmpSchemaEntry.StorageColumn+']';
970
+ break;
896
971
  default:
897
972
  if (tmpCreateSet != '')
898
973
  {
@@ -131,6 +131,30 @@ var FoxHoundDialectMySQL = function(pFable)
131
131
  return "`" + cleanseQuoting(pFieldNames[0]) + "`";
132
132
  }
133
133
 
134
+ var resolveJsonColumnPath = function(pColumnName, pSchema)
135
+ {
136
+ if (!Array.isArray(pSchema) || pSchema.length < 1) return null;
137
+
138
+ // Check for dot notation indicating JSON path: e.g. "Metadata.key" or "FableTest.Metadata.key"
139
+ var tmpParts = pColumnName.replace(/`/g, '').split('.');
140
+
141
+ for (var tmpStartIdx = 0; tmpStartIdx < Math.min(tmpParts.length - 1, 2); tmpStartIdx++)
142
+ {
143
+ var tmpBaseColumn = tmpParts[tmpStartIdx];
144
+ for (var s = 0; s < pSchema.length; s++)
145
+ {
146
+ if (pSchema[s].Column === tmpBaseColumn &&
147
+ (pSchema[s].Type === 'JSON' || pSchema[s].Type === 'JSONProxy'))
148
+ {
149
+ var tmpActualColumn = (pSchema[s].Type === 'JSONProxy') ? pSchema[s].StorageColumn : tmpBaseColumn;
150
+ var tmpJsonPath = '$.' + tmpParts.slice(tmpStartIdx + 1).join('.');
151
+ return { column: tmpActualColumn, path: tmpJsonPath };
152
+ }
153
+ }
154
+ }
155
+ return null;
156
+ };
157
+
134
158
  /**
135
159
  * Generate a query from the array of where clauses
136
160
  *
@@ -247,8 +271,18 @@ var FoxHoundDialectMySQL = function(pFable)
247
271
  else
248
272
  {
249
273
  tmpColumnParameter = tmpFilter[i].Parameter+'_w'+i;
250
- // Add the column name, operator and parameter name to the list of where value parenthetical
251
- tmpWhere += ' '+tmpFilter[i].Column+' '+tmpFilter[i].Operator+' :'+tmpColumnParameter;
274
+ // Check for JSON path references (e.g. Metadata.habitat)
275
+ var tmpSchema = Array.isArray(pParameters.query.schema) ? pParameters.query.schema : [];
276
+ var tmpJsonRef = resolveJsonColumnPath(tmpFilter[i].Column, tmpSchema);
277
+ if (tmpJsonRef)
278
+ {
279
+ tmpWhere += ' JSON_EXTRACT(`'+tmpJsonRef.column+"`, '"+tmpJsonRef.path+"') "+tmpFilter[i].Operator+' :'+tmpColumnParameter;
280
+ }
281
+ else
282
+ {
283
+ // Add the column name, operator and parameter name to the list of where value parenthetical
284
+ tmpWhere += ' '+tmpFilter[i].Column+' '+tmpFilter[i].Operator+' :'+tmpColumnParameter;
285
+ }
252
286
  pParameters.query.parameters[tmpColumnParameter] = tmpFilter[i].Value;
253
287
  }
254
288
  }
@@ -441,6 +475,20 @@ var FoxHoundDialectMySQL = function(pFable)
441
475
  // Set the query parameter
442
476
  pParameters.query.parameters[tmpColumnParameter] = pParameters.query.IDUser;
443
477
  break;
478
+ case 'JSON':
479
+ var tmpJSONUpdateParam = tmpColumn+'_'+tmpCurrentColumn;
480
+ tmpUpdate += ' '+tmpColumn+' = :'+tmpJSONUpdateParam;
481
+ pParameters.query.parameters[tmpJSONUpdateParam] = (typeof tmpRecords[0][tmpColumn] === 'string')
482
+ ? tmpRecords[0][tmpColumn]
483
+ : JSON.stringify(tmpRecords[0][tmpColumn] || {});
484
+ break;
485
+ case 'JSONProxy':
486
+ var tmpProxyUpdateParam = tmpSchemaEntry.StorageColumn+'_'+tmpCurrentColumn;
487
+ tmpUpdate += ' '+tmpSchemaEntry.StorageColumn+' = :'+tmpProxyUpdateParam;
488
+ pParameters.query.parameters[tmpProxyUpdateParam] = (typeof tmpRecords[0][tmpColumn] === 'string')
489
+ ? tmpRecords[0][tmpColumn]
490
+ : JSON.stringify(tmpRecords[0][tmpColumn] || {});
491
+ break;
444
492
  default:
445
493
  var tmpColumnDefaultParameter = tmpColumn+'_'+tmpCurrentColumn;
446
494
  tmpUpdate += ' '+tmpColumn+' = :'+tmpColumnDefaultParameter;
@@ -733,6 +781,20 @@ var FoxHoundDialectMySQL = function(pFable)
733
781
  pParameters.query.parameters[tmpColumnParameter] = pParameters.query.IDUser;
734
782
  }
735
783
  break;
784
+ case 'JSON':
785
+ var tmpJSONCreateParam = tmpColumn+'_'+tmpCurrentColumn;
786
+ tmpCreateSet += ' :'+tmpJSONCreateParam;
787
+ pParameters.query.parameters[tmpJSONCreateParam] = (typeof tmpRecords[0][tmpColumn] === 'string')
788
+ ? tmpRecords[0][tmpColumn]
789
+ : JSON.stringify(tmpRecords[0][tmpColumn] || {});
790
+ break;
791
+ case 'JSONProxy':
792
+ var tmpProxyCreateParam = tmpColumn+'_'+tmpCurrentColumn;
793
+ tmpCreateSet += ' :'+tmpProxyCreateParam;
794
+ pParameters.query.parameters[tmpProxyCreateParam] = (typeof tmpRecords[0][tmpColumn] === 'string')
795
+ ? tmpRecords[0][tmpColumn]
796
+ : JSON.stringify(tmpRecords[0][tmpColumn] || {});
797
+ break;
736
798
  default:
737
799
  buildDefaultDefinition();
738
800
  break;
@@ -793,6 +855,20 @@ var FoxHoundDialectMySQL = function(pFable)
793
855
  }
794
856
  switch (tmpSchemaEntry.Type)
795
857
  {
858
+ case 'JSON':
859
+ if (tmpCreateSet != '')
860
+ {
861
+ tmpCreateSet += ',';
862
+ }
863
+ tmpCreateSet += ' '+tmpColumn;
864
+ break;
865
+ case 'JSONProxy':
866
+ if (tmpCreateSet != '')
867
+ {
868
+ tmpCreateSet += ',';
869
+ }
870
+ tmpCreateSet += ' '+tmpSchemaEntry.StorageColumn;
871
+ break;
796
872
  default:
797
873
  if (tmpCreateSet != '')
798
874
  {
@@ -128,6 +128,27 @@ var FoxHoundDialectPostgreSQL = function(pFable)
128
128
  return '"' + cleanseQuoting(pFieldNames[0]) + '"';
129
129
  }
130
130
 
131
+ var resolveJsonColumnPath = function(pColumnName, pSchema)
132
+ {
133
+ if (!Array.isArray(pSchema) || pSchema.length < 1) return null;
134
+ var tmpParts = pColumnName.replace(/`/g, '').replace(/"/g, '').split('.');
135
+ for (var tmpStartIdx = 0; tmpStartIdx < Math.min(tmpParts.length - 1, 2); tmpStartIdx++)
136
+ {
137
+ var tmpBaseColumn = tmpParts[tmpStartIdx];
138
+ for (var s = 0; s < pSchema.length; s++)
139
+ {
140
+ if (pSchema[s].Column === tmpBaseColumn &&
141
+ (pSchema[s].Type === 'JSON' || pSchema[s].Type === 'JSONProxy'))
142
+ {
143
+ var tmpActualColumn = (pSchema[s].Type === 'JSONProxy') ? pSchema[s].StorageColumn : tmpBaseColumn;
144
+ var tmpJsonPath = '$.' + tmpParts.slice(tmpStartIdx + 1).join('.');
145
+ return { column: tmpActualColumn, path: tmpJsonPath };
146
+ }
147
+ }
148
+ }
149
+ return null;
150
+ };
151
+
131
152
  /**
132
153
  * Generate a query from the array of where clauses
133
154
  *
@@ -234,7 +255,24 @@ var FoxHoundDialectPostgreSQL = function(pFable)
234
255
  else
235
256
  {
236
257
  tmpColumnParameter = tmpFilter[i].Parameter+'_w'+i;
237
- tmpWhere += ' '+generateSafeFieldName(tmpFilter[i].Column)+' '+tmpFilter[i].Operator+' :'+tmpColumnParameter;
258
+ var tmpSchema = Array.isArray(pParameters.query.schema) ? pParameters.query.schema : [];
259
+ var tmpJsonRef = resolveJsonColumnPath(tmpFilter[i].Column, tmpSchema);
260
+ if (tmpJsonRef)
261
+ {
262
+ var tmpPathParts = tmpJsonRef.path.replace('$.', '').split('.');
263
+ if (tmpPathParts.length === 1)
264
+ {
265
+ tmpWhere += ' "'+tmpJsonRef.column+'"'+"->>'"+tmpPathParts[0]+"' "+tmpFilter[i].Operator+' :'+tmpColumnParameter;
266
+ }
267
+ else
268
+ {
269
+ tmpWhere += ' "'+tmpJsonRef.column+'"'+"#>>'{"+tmpPathParts.join(',')+"}' "+tmpFilter[i].Operator+' :'+tmpColumnParameter;
270
+ }
271
+ }
272
+ else
273
+ {
274
+ tmpWhere += ' '+generateSafeFieldName(tmpFilter[i].Column)+' '+tmpFilter[i].Operator+' :'+tmpColumnParameter;
275
+ }
238
276
  pParameters.query.parameters[tmpColumnParameter] = tmpFilter[i].Value;
239
277
  }
240
278
  }
@@ -394,6 +432,20 @@ var FoxHoundDialectPostgreSQL = function(pFable)
394
432
  tmpUpdate += ' '+generateSafeFieldName(tmpColumn)+' = :'+tmpColumnParameter;
395
433
  pParameters.query.parameters[tmpColumnParameter] = pParameters.query.IDUser;
396
434
  break;
435
+ case 'JSON':
436
+ var tmpJSONUpdateParam = tmpColumn+'_'+tmpCurrentColumn;
437
+ tmpUpdate += ' '+generateSafeFieldName(tmpColumn)+' = :'+tmpJSONUpdateParam;
438
+ pParameters.query.parameters[tmpJSONUpdateParam] = (typeof tmpRecords[0][tmpColumn] === 'string')
439
+ ? tmpRecords[0][tmpColumn]
440
+ : JSON.stringify(tmpRecords[0][tmpColumn] || {});
441
+ break;
442
+ case 'JSONProxy':
443
+ var tmpProxyUpdateParam = tmpSchemaEntry.StorageColumn+'_'+tmpCurrentColumn;
444
+ tmpUpdate += ' '+generateSafeFieldName(tmpSchemaEntry.StorageColumn)+' = :'+tmpProxyUpdateParam;
445
+ pParameters.query.parameters[tmpProxyUpdateParam] = (typeof tmpRecords[0][tmpColumn] === 'string')
446
+ ? tmpRecords[0][tmpColumn]
447
+ : JSON.stringify(tmpRecords[0][tmpColumn] || {});
448
+ break;
397
449
  default:
398
450
  var tmpColumnDefaultParameter = tmpColumn+'_'+tmpCurrentColumn;
399
451
  tmpUpdate += ' '+generateSafeFieldName(tmpColumn)+' = :'+tmpColumnDefaultParameter;
@@ -646,6 +698,20 @@ var FoxHoundDialectPostgreSQL = function(pFable)
646
698
  pParameters.query.parameters[tmpColumnParameter] = pParameters.query.IDUser;
647
699
  }
648
700
  break;
701
+ case 'JSON':
702
+ var tmpJSONCreateParam = tmpColumn+'_'+tmpCurrentColumn;
703
+ tmpCreateSet += ' :'+tmpJSONCreateParam;
704
+ pParameters.query.parameters[tmpJSONCreateParam] = (typeof tmpRecords[0][tmpColumn] === 'string')
705
+ ? tmpRecords[0][tmpColumn]
706
+ : JSON.stringify(tmpRecords[0][tmpColumn] || {});
707
+ break;
708
+ case 'JSONProxy':
709
+ var tmpProxyCreateParam = tmpColumn+'_'+tmpCurrentColumn;
710
+ tmpCreateSet += ' :'+tmpProxyCreateParam;
711
+ pParameters.query.parameters[tmpProxyCreateParam] = (typeof tmpRecords[0][tmpColumn] === 'string')
712
+ ? tmpRecords[0][tmpColumn]
713
+ : JSON.stringify(tmpRecords[0][tmpColumn] || {});
714
+ break;
649
715
  default:
650
716
  buildDefaultDefinition();
651
717
  break;
@@ -697,6 +763,20 @@ var FoxHoundDialectPostgreSQL = function(pFable)
697
763
  }
698
764
  switch (tmpSchemaEntry.Type)
699
765
  {
766
+ case 'JSON':
767
+ if (tmpCreateSet != '')
768
+ {
769
+ tmpCreateSet += ',';
770
+ }
771
+ tmpCreateSet += ' '+generateSafeFieldName(tmpColumn);
772
+ break;
773
+ case 'JSONProxy':
774
+ if (tmpCreateSet != '')
775
+ {
776
+ tmpCreateSet += ',';
777
+ }
778
+ tmpCreateSet += ' '+generateSafeFieldName(tmpSchemaEntry.StorageColumn);
779
+ break;
700
780
  default:
701
781
  if (tmpCreateSet != '')
702
782
  {
@@ -115,6 +115,27 @@ var FoxHoundDialectSQLite = function(pFable)
115
115
  return tmpFieldList;
116
116
  };
117
117
 
118
+ var resolveJsonColumnPath = function(pColumnName, pSchema)
119
+ {
120
+ if (!Array.isArray(pSchema) || pSchema.length < 1) return null;
121
+ var tmpParts = pColumnName.replace(/`/g, '').replace(/"/g, '').split('.');
122
+ for (var tmpStartIdx = 0; tmpStartIdx < Math.min(tmpParts.length - 1, 2); tmpStartIdx++)
123
+ {
124
+ var tmpBaseColumn = tmpParts[tmpStartIdx];
125
+ for (var s = 0; s < pSchema.length; s++)
126
+ {
127
+ if (pSchema[s].Column === tmpBaseColumn &&
128
+ (pSchema[s].Type === 'JSON' || pSchema[s].Type === 'JSONProxy'))
129
+ {
130
+ var tmpActualColumn = (pSchema[s].Type === 'JSONProxy') ? pSchema[s].StorageColumn : tmpBaseColumn;
131
+ var tmpJsonPath = '$.' + tmpParts.slice(tmpStartIdx + 1).join('.');
132
+ return { column: tmpActualColumn, path: tmpJsonPath };
133
+ }
134
+ }
135
+ }
136
+ return null;
137
+ };
138
+
118
139
  /**
119
140
  * Generate a query from the array of where clauses
120
141
  *
@@ -243,8 +264,16 @@ var FoxHoundDialectSQLite = function(pFable)
243
264
  else
244
265
  {
245
266
  tmpColumnParameter = tmpFilter[i].Parameter+'_w'+i;
246
- // Add the column name, operator and parameter name to the list of where value parenthetical
247
- tmpWhere += ' '+escapeColumn(tmpFilter[i].Column, pParameters)+' '+tmpFilter[i].Operator+' :'+tmpColumnParameter;
267
+ var tmpSchema = Array.isArray(pParameters.query.schema) ? pParameters.query.schema : [];
268
+ var tmpJsonRef = resolveJsonColumnPath(tmpFilter[i].Column, tmpSchema);
269
+ if (tmpJsonRef)
270
+ {
271
+ tmpWhere += ' json_extract(`'+tmpJsonRef.column+"`, '"+tmpJsonRef.path+"') "+tmpFilter[i].Operator+' :'+tmpColumnParameter;
272
+ }
273
+ else
274
+ {
275
+ tmpWhere += ' '+escapeColumn(tmpFilter[i].Column, pParameters)+' '+tmpFilter[i].Operator+' :'+tmpColumnParameter;
276
+ }
248
277
  pParameters.query.parameters[tmpColumnParameter] = tmpFilter[i].Value;
249
278
  }
250
279
  }
@@ -392,6 +421,20 @@ var FoxHoundDialectSQLite = function(pFable)
392
421
  // Set the query parameter
393
422
  pParameters.query.parameters[tmpColumnParameter] = pParameters.query.IDUser;
394
423
  break;
424
+ case 'JSON':
425
+ var tmpJSONUpdateParam = tmpColumn+'_'+tmpCurrentColumn;
426
+ tmpUpdate += ' '+tmpColumn+' = :'+tmpJSONUpdateParam;
427
+ pParameters.query.parameters[tmpJSONUpdateParam] = (typeof tmpRecords[0][tmpColumn] === 'string')
428
+ ? tmpRecords[0][tmpColumn]
429
+ : JSON.stringify(tmpRecords[0][tmpColumn] || {});
430
+ break;
431
+ case 'JSONProxy':
432
+ var tmpProxyUpdateParam = tmpSchemaEntry.StorageColumn+'_'+tmpCurrentColumn;
433
+ tmpUpdate += ' '+tmpSchemaEntry.StorageColumn+' = :'+tmpProxyUpdateParam;
434
+ pParameters.query.parameters[tmpProxyUpdateParam] = (typeof tmpRecords[0][tmpColumn] === 'string')
435
+ ? tmpRecords[0][tmpColumn]
436
+ : JSON.stringify(tmpRecords[0][tmpColumn] || {});
437
+ break;
395
438
  default:
396
439
  var tmpColumnDefaultParameter = tmpColumn+'_'+tmpCurrentColumn;
397
440
  tmpUpdate += ' '+escapeColumn(tmpColumn, pParameters)+' = :'+tmpColumnDefaultParameter;
@@ -687,6 +730,20 @@ var FoxHoundDialectSQLite = function(pFable)
687
730
  pParameters.query.parameters[tmpColumnParameter] = pParameters.query.IDUser;
688
731
  }
689
732
  break;
733
+ case 'JSON':
734
+ var tmpJSONCreateParam = tmpColumn+'_'+tmpCurrentColumn;
735
+ tmpCreateSet += ' :'+tmpJSONCreateParam;
736
+ pParameters.query.parameters[tmpJSONCreateParam] = (typeof tmpRecords[0][tmpColumn] === 'string')
737
+ ? tmpRecords[0][tmpColumn]
738
+ : JSON.stringify(tmpRecords[0][tmpColumn] || {});
739
+ break;
740
+ case 'JSONProxy':
741
+ var tmpProxyCreateParam = tmpColumn+'_'+tmpCurrentColumn;
742
+ tmpCreateSet += ' :'+tmpProxyCreateParam;
743
+ pParameters.query.parameters[tmpProxyCreateParam] = (typeof tmpRecords[0][tmpColumn] === 'string')
744
+ ? tmpRecords[0][tmpColumn]
745
+ : JSON.stringify(tmpRecords[0][tmpColumn] || {});
746
+ break;
690
747
  default:
691
748
  buildDefaultDefinition();
692
749
  break;
@@ -747,6 +804,20 @@ var FoxHoundDialectSQLite = function(pFable)
747
804
  }
748
805
  switch (tmpSchemaEntry.Type)
749
806
  {
807
+ case 'JSON':
808
+ if (tmpCreateSet != '')
809
+ {
810
+ tmpCreateSet += ',';
811
+ }
812
+ tmpCreateSet += ' '+tmpColumn;
813
+ break;
814
+ case 'JSONProxy':
815
+ if (tmpCreateSet != '')
816
+ {
817
+ tmpCreateSet += ',';
818
+ }
819
+ tmpCreateSet += ' '+tmpSchemaEntry.StorageColumn;
820
+ break;
750
821
  default:
751
822
  if (tmpCreateSet != '')
752
823
  {