jsonbadger 0.5.0 → 0.6.0

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 (123) hide show
  1. package/README.md +36 -18
  2. package/docs/api/connection.md +144 -0
  3. package/docs/api/delta-tracker.md +106 -0
  4. package/docs/api/document.md +77 -0
  5. package/docs/api/field-types.md +329 -0
  6. package/docs/api/index.md +35 -0
  7. package/docs/api/model.md +392 -0
  8. package/docs/api/query-builder.md +81 -0
  9. package/docs/api/schema.md +204 -0
  10. package/docs/architecture-flow.md +397 -0
  11. package/docs/examples.md +495 -218
  12. package/docs/jsonb-ops.md +171 -0
  13. package/docs/lifecycle/model-compilation.md +111 -0
  14. package/docs/lifecycle.md +146 -0
  15. package/docs/query-translation.md +11 -10
  16. package/package.json +10 -3
  17. package/src/connection/connect.js +12 -17
  18. package/src/connection/connection.js +128 -0
  19. package/src/connection/server-capabilities.js +60 -59
  20. package/src/constants/defaults.js +32 -19
  21. package/src/constants/{id-strategies.js → id-strategy.js} +28 -29
  22. package/src/constants/intake-mode.js +8 -0
  23. package/src/debug/debug-logger.js +17 -15
  24. package/src/errors/model-overwrite-error.js +25 -0
  25. package/src/errors/query-error.js +25 -23
  26. package/src/errors/validation-error.js +25 -23
  27. package/src/field-types/base-field-type.js +137 -140
  28. package/src/field-types/builtins/advanced.js +365 -365
  29. package/src/field-types/builtins/index.js +579 -585
  30. package/src/field-types/field-type-namespace.js +9 -0
  31. package/src/field-types/registry.js +149 -122
  32. package/src/index.js +26 -36
  33. package/src/migration/ensure-index.js +157 -154
  34. package/src/migration/ensure-schema.js +27 -15
  35. package/src/migration/ensure-table.js +44 -31
  36. package/src/migration/schema-indexes-resolver.js +8 -6
  37. package/src/model/document-instance.js +29 -540
  38. package/src/model/document.js +60 -0
  39. package/src/model/factory/constants.js +36 -0
  40. package/src/model/factory/index.js +58 -0
  41. package/src/model/model.js +875 -0
  42. package/src/model/operations/delete-one.js +39 -0
  43. package/src/model/operations/insert-one.js +35 -0
  44. package/src/model/operations/query-builder.js +132 -0
  45. package/src/model/operations/update-one.js +333 -0
  46. package/src/model/state.js +34 -0
  47. package/src/schema/field-definition-parser.js +213 -218
  48. package/src/schema/path-introspection.js +87 -82
  49. package/src/schema/schema-compiler.js +126 -212
  50. package/src/schema/schema.js +621 -138
  51. package/src/sql/index.js +17 -0
  52. package/src/sql/jsonb/ops.js +153 -0
  53. package/src/{query → sql/jsonb}/path-parser.js +54 -43
  54. package/src/sql/jsonb/read/elem-match.js +133 -0
  55. package/src/{query → sql/jsonb/read}/operators/contains.js +13 -7
  56. package/src/sql/jsonb/read/operators/elem-match.js +9 -0
  57. package/src/{query → sql/jsonb/read}/operators/has-all-keys.js +17 -11
  58. package/src/{query → sql/jsonb/read}/operators/has-any-keys.js +18 -11
  59. package/src/sql/jsonb/read/operators/has-key.js +12 -0
  60. package/src/{query → sql/jsonb/read}/operators/jsonpath-exists.js +22 -15
  61. package/src/{query → sql/jsonb/read}/operators/jsonpath-match.js +22 -15
  62. package/src/{query → sql/jsonb/read}/operators/size.js +23 -16
  63. package/src/sql/parameter-binder.js +18 -13
  64. package/src/sql/read/build-count-query.js +12 -0
  65. package/src/sql/read/build-find-query.js +25 -0
  66. package/src/sql/read/limit-skip.js +21 -0
  67. package/src/sql/read/sort.js +85 -0
  68. package/src/sql/read/where/base-fields.js +310 -0
  69. package/src/sql/read/where/casting.js +90 -0
  70. package/src/sql/read/where/context.js +79 -0
  71. package/src/sql/read/where/field-clause.js +58 -0
  72. package/src/sql/read/where/index.js +38 -0
  73. package/src/sql/read/where/operator-entries.js +29 -0
  74. package/src/{query → sql/read/where}/operators/all.js +16 -10
  75. package/src/sql/read/where/operators/eq.js +12 -0
  76. package/src/{query → sql/read/where}/operators/gt.js +23 -16
  77. package/src/{query → sql/read/where}/operators/gte.js +23 -16
  78. package/src/{query → sql/read/where}/operators/in.js +18 -12
  79. package/src/sql/read/where/operators/index.js +40 -0
  80. package/src/{query → sql/read/where}/operators/lt.js +23 -16
  81. package/src/{query → sql/read/where}/operators/lte.js +23 -16
  82. package/src/sql/read/where/operators/ne.js +12 -0
  83. package/src/{query → sql/read/where}/operators/nin.js +18 -12
  84. package/src/{query → sql/read/where}/operators/regex.js +14 -8
  85. package/src/sql/read/where/operators.js +126 -0
  86. package/src/sql/read/where/text-operators.js +83 -0
  87. package/src/sql/run.js +46 -0
  88. package/src/sql/write/build-delete-query.js +33 -0
  89. package/src/sql/write/build-insert-query.js +42 -0
  90. package/src/sql/write/build-update-query.js +65 -0
  91. package/src/utils/assert.js +34 -27
  92. package/src/utils/delta-tracker/.archive/1 tracker-redesign-codex-v2.md +250 -0
  93. package/src/utils/delta-tracker/.archive/1 tracker-redesign-gemini.md +101 -0
  94. package/src/utils/delta-tracker/.archive/2 evaluation by gemini.txt +65 -0
  95. package/src/utils/delta-tracker/.archive/2 evaluation by grok.txt +39 -0
  96. package/src/utils/delta-tracker/.archive/3 gemini evaluate grok.txt +37 -0
  97. package/src/utils/delta-tracker/.archive/3 grok evaluate gemini.txt +63 -0
  98. package/src/utils/delta-tracker/.archive/4 gemini veredict.txt +16 -0
  99. package/src/utils/delta-tracker/.archive/index.1.js +587 -0
  100. package/src/utils/delta-tracker/.archive/index.2.js +612 -0
  101. package/src/utils/delta-tracker/index.js +592 -0
  102. package/src/utils/dirty-tracker/inline.js +335 -0
  103. package/src/utils/dirty-tracker/instance.js +414 -0
  104. package/src/utils/dirty-tracker/static.js +343 -0
  105. package/src/utils/json-safe.js +13 -9
  106. package/src/utils/object-path.js +227 -33
  107. package/src/utils/object.js +408 -168
  108. package/src/utils/string.js +55 -0
  109. package/src/utils/value.js +169 -30
  110. package/docs/api.md +0 -152
  111. package/src/connection/disconnect.js +0 -16
  112. package/src/connection/pool-store.js +0 -46
  113. package/src/model/model-factory.js +0 -555
  114. package/src/query/limit-skip-compiler.js +0 -31
  115. package/src/query/operators/elem-match.js +0 -3
  116. package/src/query/operators/eq.js +0 -6
  117. package/src/query/operators/has-key.js +0 -6
  118. package/src/query/operators/index.js +0 -60
  119. package/src/query/operators/ne.js +0 -6
  120. package/src/query/query-builder.js +0 -93
  121. package/src/query/sort-compiler.js +0 -30
  122. package/src/query/where-compiler.js +0 -477
  123. package/src/sql/sql-runner.js +0 -31
@@ -0,0 +1,12 @@
1
+ /*
2
+ * MODULE RESPONSIBILITY
3
+ * Build SQL text and parameters for count read queries.
4
+ */
5
+ function build_count_query(query_context) {
6
+ return {
7
+ sql_text: `SELECT COUNT(*)::int AS total_count FROM ${query_context.table_identifier} WHERE ${query_context.where_result.sql}`,
8
+ sql_params: query_context.where_result.params
9
+ };
10
+ }
11
+
12
+ export default build_count_query;
@@ -0,0 +1,25 @@
1
+ /*
2
+ * MODULE RESPONSIBILITY
3
+ * Build SQL text and parameters for row-returning read queries.
4
+ */
5
+ import limit_skip_compiler from '#src/sql/read/limit-skip.js';
6
+ import sort_compiler from '#src/sql/read/sort.js';
7
+
8
+ function build_find_query(query_context) {
9
+ let sql_text =
10
+ `SELECT id::text AS id, ${query_context.data_identifier} AS data, ` +
11
+ `created_at AS created_at, ` +
12
+ `updated_at AS updated_at ` +
13
+ `FROM ${query_context.table_identifier} ` +
14
+ `WHERE ${query_context.where_result.sql}`;
15
+
16
+ sql_text += sort_compiler(query_context.sort, {data_column: query_context.data_column});
17
+ sql_text += limit_skip_compiler(query_context.limit_count, query_context.skip_count);
18
+
19
+ return {
20
+ sql_text,
21
+ sql_params: query_context.where_result.params
22
+ };
23
+ }
24
+
25
+ export default build_find_query;
@@ -0,0 +1,21 @@
1
+ /*
2
+ * MODULE RESPONSIBILITY
3
+ * Compile LIMIT and OFFSET clauses for read queries.
4
+ */
5
+ import {is_nan, to_int} from '#src/utils/value.js';
6
+
7
+ function limit_skip_compiler(limit_value, skip_value) {
8
+ let sql_fragment = '';
9
+
10
+ if(!is_nan(limit_value)) {
11
+ sql_fragment += ' LIMIT ' + to_int(limit_value);
12
+ }
13
+
14
+ if(!is_nan(skip_value)) {
15
+ sql_fragment += ' OFFSET ' + to_int(skip_value);
16
+ }
17
+
18
+ return sql_fragment;
19
+ }
20
+
21
+ export default limit_skip_compiler;
@@ -0,0 +1,85 @@
1
+ /*
2
+ * MODULE RESPONSIBILITY
3
+ * Compile ORDER BY clauses for read queries.
4
+ */
5
+ import QueryError from '#src/errors/query-error.js';
6
+ import {quote_identifier} from '#src/utils/assert.js';
7
+ import {has_own} from '#src/utils/object.js';
8
+ import {build_text_expression} from '#src/sql/jsonb/path-parser.js';
9
+ import {split_dot_path} from '#src/utils/object-path.js';
10
+
11
+ const base_field_sort_columns = Object.freeze({
12
+ id: '"id"',
13
+ created_at: '"created_at"',
14
+ updated_at: '"updated_at"'
15
+ });
16
+
17
+ /**
18
+ * Compiles a sort object into a SQL ORDER BY clause.
19
+ *
20
+ * Supported shape:
21
+ * - keys: field paths (e.g. `name`, `profile.last_name`, `created_at`)
22
+ * - values: order hints (`-1` => DESC, any other value => ASC)
23
+ *
24
+ * Returns an empty string for missing/invalid sort definitions so callers can
25
+ * safely append the result to a larger SQL fragment.
26
+ */
27
+ function sort_compiler(sort_definition, compile_options) {
28
+ if(!sort_definition || typeof sort_definition !== 'object') {
29
+ return '';
30
+ }
31
+
32
+ const data_column_name = compile_options?.data_column ?? 'data';
33
+ const data_column_reference = quote_identifier(data_column_name);
34
+ const sort_entries = Object.entries(sort_definition);
35
+ const sort_clauses = [];
36
+ let entry_index = 0;
37
+
38
+ while(entry_index < sort_entries.length) {
39
+ const sort_entry = sort_entries[entry_index];
40
+ const path_value = sort_entry[0];
41
+ // Keep sort order normalization permissive: only exact -1 maps to DESC.
42
+ const order_value = Number(sort_entry[1]) === -1 ? 'DESC' : 'ASC';
43
+ const text_expression = resolve_sort_expression(path_value, data_column_reference);
44
+
45
+ sort_clauses.push(text_expression + ' ' + order_value);
46
+ entry_index += 1;
47
+ }
48
+
49
+ if(sort_clauses.length === 0) {
50
+ return '';
51
+ }
52
+
53
+ return ' ORDER BY ' + sort_clauses.join(', ');
54
+ }
55
+
56
+ /**
57
+ * Resolves a sort path to the SQL expression used in ORDER BY.
58
+ *
59
+ * Base fields (`id`, `created_at`, `updated_at`) are mapped to
60
+ * dedicated table columns instead of JSON path lookups. These fields are
61
+ * intentionally restricted to top-level usage only: `created_at` is valid,
62
+ * but `created_at.anything` is rejected.
63
+ *
64
+ * Non-base-field paths are treated as JSON data paths and may be dotted.
65
+ */
66
+ function resolve_sort_expression(path_value, data_column_reference) {
67
+ const path_segments = split_dot_path(path_value);
68
+ const root_path = path_segments[0];
69
+
70
+ if(has_own(base_field_sort_columns, root_path)) {
71
+ // Prevent ambiguity: base-field columns do not support dotted access.
72
+ if(path_segments.length > 1) {
73
+ throw new QueryError('Base fields only support top-level sort paths', {
74
+ path: path_value,
75
+ field: root_path
76
+ });
77
+ }
78
+
79
+ return base_field_sort_columns[root_path];
80
+ }
81
+
82
+ return build_text_expression(data_column_reference, path_value);
83
+ }
84
+
85
+ export default sort_compiler;
@@ -0,0 +1,310 @@
1
+ /*
2
+ * MODULE RESPONSIBILITY
3
+ * Compile top-level base-field predicates for read queries.
4
+ */
5
+ import QueryError from '#src/errors/query-error.js';
6
+ import ID_STRATEGY from '#src/constants/id-strategy.js';
7
+ import {bind_parameter} from '#src/sql/parameter-binder.js';
8
+ import {parse_path} from '#src/sql/jsonb/path-parser.js';
9
+ import {is_plain_object, is_uuid_v7} from '#src/utils/value.js';
10
+
11
+ // Base fields are top-level only. Each one owns its SQL expression and allowed operators.
12
+ const base_field_rules = Object.freeze({
13
+ id: {
14
+ expression: '"id"',
15
+ operator_allowlist: new Set(['$eq', '$ne', '$in', '$nin']),
16
+ value_kind: 'id'
17
+ },
18
+ created_at: {
19
+ expression: '"created_at"',
20
+ operator_allowlist: new Set(['$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin']),
21
+ value_kind: 'timestamp'
22
+ },
23
+ updated_at: {
24
+ expression: '"updated_at"',
25
+ operator_allowlist: new Set(['$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin']),
26
+ value_kind: 'timestamp'
27
+ }
28
+ });
29
+
30
+ /**
31
+ * Resolve the base-field name for a query path.
32
+ *
33
+ * @param {string} path_value Query path.
34
+ * @returns {string|null}
35
+ * @throws {QueryError} When a base field is used with a nested path.
36
+ */
37
+ function resolve_base_field_name(path_value) {
38
+ const path_info = parse_path(path_value);
39
+ const root_path = path_info.root_path;
40
+
41
+ if(!base_field_rules[root_path]) {
42
+ return null;
43
+ }
44
+
45
+ if(path_info.is_nested) {
46
+ throw new QueryError('Base fields only support top-level paths', {
47
+ path: path_value,
48
+ field: root_path
49
+ });
50
+ }
51
+
52
+ return root_path;
53
+ }
54
+
55
+ /**
56
+ * Compile one base-field clause.
57
+ *
58
+ * @param {string} field_name Base-field name.
59
+ * @param {*} comparison_value Query value or operator object.
60
+ * @param {object} compile_context Normalized where-compiler context.
61
+ * @param {object} parameter_state SQL parameter state.
62
+ * @returns {string}
63
+ * @throws {QueryError} When the value or operator is invalid for the base field.
64
+ */
65
+ function compile_base_field_clause(field_name, comparison_value, compile_context, parameter_state) {
66
+ if(comparison_value instanceof RegExp) {
67
+ throw new QueryError('Base field does not support regular expression matching', {
68
+ field: field_name
69
+ });
70
+ }
71
+
72
+ if(is_plain_object(comparison_value)) {
73
+ if(comparison_value.$elem_match !== undefined) {
74
+ throw new QueryError('Base field does not support $elem_match', {
75
+ field: field_name
76
+ });
77
+ }
78
+
79
+ if(!has_operator_entries(comparison_value)) {
80
+ throw new QueryError('Base field only supports scalar values or operator objects', {
81
+ field: field_name
82
+ });
83
+ }
84
+
85
+ const clause_list = [];
86
+ const operator_entries = Object.entries(comparison_value);
87
+ let operator_index = 0;
88
+
89
+ while(operator_index < operator_entries.length) {
90
+ const operator_entry = operator_entries[operator_index];
91
+ const operator_name = operator_entry[0];
92
+ const operator_value = operator_entry[1];
93
+
94
+ if(operator_name === '$options') {
95
+ throw new QueryError('Base field does not support $options', {
96
+ field: field_name
97
+ });
98
+ }
99
+
100
+ assert_base_field_operator_supported(field_name, operator_name);
101
+ clause_list.push(
102
+ compile_base_field_operator(field_name, operator_name, operator_value, compile_context, parameter_state)
103
+ );
104
+ operator_index += 1;
105
+ }
106
+
107
+ return clause_list.join(' AND ');
108
+ }
109
+
110
+ const normalized_value = normalize_base_field_scalar(field_name, comparison_value, '$eq', compile_context);
111
+ return build_base_field_comparison(field_name, '$eq', normalized_value, compile_context, parameter_state);
112
+ }
113
+
114
+ function compile_base_field_operator(field_name, operator_name, operator_value, compile_context, parameter_state) {
115
+ if(operator_name === '$in' || operator_name === '$nin') {
116
+ const normalized_values = normalize_base_field_list(field_name, operator_value, operator_name, compile_context);
117
+ return build_base_field_comparison(field_name, operator_name, normalized_values, compile_context, parameter_state);
118
+ }
119
+
120
+ const normalized_value = normalize_base_field_scalar(field_name, operator_value, operator_name, compile_context);
121
+ return build_base_field_comparison(field_name, operator_name, normalized_value, compile_context, parameter_state);
122
+ }
123
+
124
+ function build_base_field_comparison(field_name, operator_name, normalized_value, compile_context, parameter_state) {
125
+ const field_rule = base_field_rules[field_name];
126
+ const placeholder = bind_parameter(parameter_state, normalized_value);
127
+ const field_expression = field_rule.expression;
128
+ const id_parameter_cast = resolve_id_parameter_cast(compile_context.id_strategy);
129
+
130
+ if(operator_name === '$eq') {
131
+ if(field_rule.value_kind === 'id') {
132
+ return field_expression + ' = ' + placeholder + id_parameter_cast;
133
+ }
134
+
135
+ return field_expression + ' = ' + placeholder + '::timestamptz';
136
+ }
137
+
138
+ if(operator_name === '$ne') {
139
+ if(field_rule.value_kind === 'id') {
140
+ return field_expression + ' != ' + placeholder + id_parameter_cast;
141
+ }
142
+
143
+ return field_expression + ' != ' + placeholder + '::timestamptz';
144
+ }
145
+
146
+ if(operator_name === '$gt') {
147
+ return field_expression + ' > ' + placeholder + '::timestamptz';
148
+ }
149
+
150
+ if(operator_name === '$gte') {
151
+ return field_expression + ' >= ' + placeholder + '::timestamptz';
152
+ }
153
+
154
+ if(operator_name === '$lt') {
155
+ return field_expression + ' < ' + placeholder + '::timestamptz';
156
+ }
157
+
158
+ if(operator_name === '$lte') {
159
+ return field_expression + ' <= ' + placeholder + '::timestamptz';
160
+ }
161
+
162
+ if(operator_name === '$in') {
163
+ if(field_rule.value_kind === 'id') {
164
+ const id_array_cast = id_parameter_cast === '::uuid' ? '::uuid[]' : '::bigint[]';
165
+ return field_expression + ' = ANY(' + placeholder + id_array_cast + ')';
166
+ }
167
+
168
+ return field_expression + ' = ANY(' + placeholder + '::timestamptz[])';
169
+ }
170
+
171
+ if(operator_name === '$nin') {
172
+ if(field_rule.value_kind === 'id') {
173
+ const id_array_cast = id_parameter_cast === '::uuid' ? '::uuid[]' : '::bigint[]';
174
+ return 'NOT (' + field_expression + ' = ANY(' + placeholder + id_array_cast + '))';
175
+ }
176
+
177
+ return 'NOT (' + field_expression + ' = ANY(' + placeholder + '::timestamptz[]))';
178
+ }
179
+ }
180
+
181
+ function normalize_base_field_list(field_name, operator_value, operator_name, compile_context) {
182
+ const input_values = Array.isArray(operator_value) ? operator_value : [operator_value];
183
+ const normalized_values = [];
184
+ let value_index = 0;
185
+
186
+ while(value_index < input_values.length) {
187
+ const next_value = normalize_base_field_scalar(field_name, input_values[value_index], operator_name, compile_context);
188
+ normalized_values.push(next_value);
189
+ value_index += 1;
190
+ }
191
+
192
+ return normalized_values;
193
+ }
194
+
195
+ function normalize_base_field_scalar(field_name, operator_value, operator_name, compile_context) {
196
+ const field_rule = base_field_rules[field_name];
197
+
198
+ if(operator_value === undefined || operator_value === null || Array.isArray(operator_value) || is_plain_object(operator_value)) {
199
+ throw new QueryError('Invalid value for base field', {
200
+ field: field_name,
201
+ operator: operator_name,
202
+ value: operator_value
203
+ });
204
+ }
205
+
206
+ if(field_rule.value_kind === 'id') {
207
+ return normalize_id_value(operator_value, operator_name, compile_context.id_strategy);
208
+ }
209
+
210
+ return normalize_timestamp_value(field_name, operator_name, operator_value);
211
+ }
212
+
213
+ function normalize_timestamp_value(field_name, operator_name, operator_value) {
214
+ const parsed_timestamp = operator_value instanceof Date ? operator_value : new Date(operator_value);
215
+
216
+ if(Number.isNaN(parsed_timestamp.getTime())) {
217
+ throw new QueryError('Invalid timestamp value for base field', {
218
+ field: field_name,
219
+ operator: operator_name,
220
+ value: operator_value
221
+ });
222
+ }
223
+
224
+ return parsed_timestamp.toISOString();
225
+ }
226
+
227
+ function normalize_id_value(operator_value, operator_name, id_strategy) {
228
+ if(id_strategy === ID_STRATEGY.uuidv7) {
229
+ const uuid_value = String(operator_value);
230
+
231
+ if(!is_uuid_v7(uuid_value)) {
232
+ throw new QueryError('Invalid id value for uuid id_strategy', {
233
+ field: 'id',
234
+ operator: operator_name,
235
+ value: operator_value
236
+ });
237
+ }
238
+
239
+ return uuid_value;
240
+ }
241
+
242
+ if(typeof operator_value === 'bigint') {
243
+ return operator_value.toString();
244
+ }
245
+
246
+ if(typeof operator_value === 'number') {
247
+ if(Number.isInteger(operator_value) === false) {
248
+ throw new QueryError('Invalid id value for bigserial id_strategy', {
249
+ field: 'id',
250
+ operator: operator_name,
251
+ value: operator_value
252
+ });
253
+ }
254
+
255
+ return String(operator_value);
256
+ }
257
+
258
+ const numeric_value = String(operator_value);
259
+
260
+ if(/^[0-9]+$/.test(numeric_value) === false) {
261
+ throw new QueryError('Invalid id value for bigserial id_strategy', {
262
+ field: 'id',
263
+ operator: operator_name,
264
+ value: operator_value
265
+ });
266
+ }
267
+
268
+ return numeric_value;
269
+ }
270
+
271
+ function resolve_id_parameter_cast(id_strategy) {
272
+ if(id_strategy === ID_STRATEGY.uuidv7) {
273
+ return '::uuid';
274
+ }
275
+
276
+ return '::bigint';
277
+ }
278
+
279
+ function assert_base_field_operator_supported(field_name, operator_name) {
280
+ const field_rule = base_field_rules[field_name];
281
+
282
+ if(field_rule.operator_allowlist.has(operator_name)) {
283
+ return;
284
+ }
285
+
286
+ throw new QueryError('Operator is not supported for base field', {
287
+ field: field_name,
288
+ operator: operator_name
289
+ });
290
+ }
291
+
292
+ function has_operator_entries(object_value) {
293
+ const keys = Object.keys(object_value);
294
+ let key_index = 0;
295
+
296
+ while(key_index < keys.length) {
297
+ if(keys[key_index][0] === '$') {
298
+ return true;
299
+ }
300
+
301
+ key_index += 1;
302
+ }
303
+
304
+ return false;
305
+ }
306
+
307
+ export {
308
+ compile_base_field_clause,
309
+ resolve_base_field_name
310
+ };
@@ -0,0 +1,90 @@
1
+ /*
2
+ * MODULE RESPONSIBILITY
3
+ * Cast read-query operator values with schema-aware rules.
4
+ */
5
+ import {is_function} from '#src/utils/value.js';
6
+
7
+ import {resolve_query_field_type} from '#src/sql/read/where/context.js';
8
+
9
+ function cast_query_value(path_value, comparison_value, compile_context) {
10
+ if(comparison_value === undefined || comparison_value === null) {
11
+ return comparison_value;
12
+ }
13
+
14
+ const field_type = resolve_query_field_type(compile_context, path_value);
15
+
16
+ if(!field_type || !is_function(field_type.cast)) {
17
+ return comparison_value;
18
+ }
19
+
20
+ return field_type.cast(comparison_value, {
21
+ path: path_value,
22
+ mode: 'query'
23
+ });
24
+ }
25
+
26
+ function cast_array_contains_value(path_value, comparison_value, compile_context) {
27
+ const field_type = resolve_query_field_type(compile_context, path_value);
28
+
29
+ if(!field_type || field_type.instance !== 'Array' || !field_type.of_field_type || !is_function(field_type.of_field_type.cast)) {
30
+ return comparison_value;
31
+ }
32
+
33
+ return field_type.of_field_type.cast(comparison_value, {
34
+ path: path_value,
35
+ mode: 'query'
36
+ });
37
+ }
38
+
39
+ function cast_operator_value(path_value, operator_name, operator_value, compile_context) {
40
+ if(
41
+ operator_name === '$eq' ||
42
+ operator_name === '$ne' ||
43
+ operator_name === '$gt' ||
44
+ operator_name === '$gte' ||
45
+ operator_name === '$lt' ||
46
+ operator_name === '$lte'
47
+ ) {
48
+ return cast_query_value(path_value, operator_value, compile_context);
49
+ }
50
+
51
+ if(operator_name === '$in' || operator_name === '$nin') {
52
+ if(!Array.isArray(operator_value)) {
53
+ return [cast_query_value(path_value, operator_value, compile_context)];
54
+ }
55
+
56
+ const casted_values = [];
57
+ let value_index = 0;
58
+
59
+ while(value_index < operator_value.length) {
60
+ casted_values.push(cast_query_value(path_value, operator_value[value_index], compile_context));
61
+ value_index += 1;
62
+ }
63
+
64
+ return casted_values;
65
+ }
66
+
67
+ if(operator_name === '$all') {
68
+ if(!Array.isArray(operator_value)) {
69
+ return operator_value;
70
+ }
71
+
72
+ const casted_values = [];
73
+ let value_index = 0;
74
+
75
+ while(value_index < operator_value.length) {
76
+ casted_values.push(cast_array_contains_value(path_value, operator_value[value_index], compile_context));
77
+ value_index += 1;
78
+ }
79
+
80
+ return casted_values;
81
+ }
82
+
83
+ return operator_value;
84
+ }
85
+
86
+ export {
87
+ cast_array_contains_value,
88
+ cast_operator_value,
89
+ cast_query_value
90
+ };
@@ -0,0 +1,79 @@
1
+ /*
2
+ * MODULE RESPONSIBILITY
3
+ * Create shared compile context helpers for read-query WHERE compilation.
4
+ */
5
+ import ID_STRATEGY from '#src/constants/id-strategy.js';
6
+
7
+ import {quote_identifier} from '#src/utils/assert.js';
8
+ import {is_function, is_object} from '#src/utils/value.js';
9
+
10
+ function resolve_query_field_type(compile_context, path_value) {
11
+ const schema_instance = compile_context.schema_instance;
12
+
13
+ if(!schema_instance || !is_function(schema_instance.get_path)) {
14
+ return null;
15
+ }
16
+
17
+ return schema_instance.get_path(path_value);
18
+ }
19
+
20
+ function should_use_array_contains(path_value, comparison_value, compile_context) {
21
+ const schema_instance = compile_context.schema_instance;
22
+
23
+ if(!schema_instance || !is_function(schema_instance.is_array_root)) {
24
+ return false;
25
+ }
26
+
27
+ if(is_object(comparison_value) || comparison_value instanceof RegExp) {
28
+ return false;
29
+ }
30
+
31
+ return schema_instance.is_array_root(path_value);
32
+ }
33
+
34
+ function is_array_root(compile_context, root_path) {
35
+ const schema_instance = compile_context.schema_instance;
36
+
37
+ if(!schema_instance || !is_function(schema_instance.is_array_root)) {
38
+ return false;
39
+ }
40
+
41
+ return schema_instance.is_array_root(root_path);
42
+ }
43
+
44
+ function create_compile_context(compile_options) {
45
+ const options = compile_options ?? {};
46
+ const data_column_name = options.data_column ?? 'data';
47
+ const data_column_reference = quote_identifier(data_column_name);
48
+ const schema_instance = options.schema ?? null;
49
+ const id_strategy = options.id_strategy === ID_STRATEGY.uuidv7 ? ID_STRATEGY.uuidv7 : ID_STRATEGY.bigserial;
50
+
51
+ return {
52
+ data_column_reference,
53
+ schema_instance,
54
+ id_strategy
55
+ };
56
+ }
57
+
58
+ function has_operator_entries(object_value) {
59
+ const keys = Object.keys(object_value);
60
+ let key_index = 0;
61
+
62
+ while(key_index < keys.length) {
63
+ if(keys[key_index][0] === '$') {
64
+ return true;
65
+ }
66
+
67
+ key_index += 1;
68
+ }
69
+
70
+ return false;
71
+ }
72
+
73
+ export {
74
+ create_compile_context,
75
+ has_operator_entries,
76
+ is_array_root,
77
+ resolve_query_field_type,
78
+ should_use_array_contains
79
+ };
@@ -0,0 +1,58 @@
1
+ /*
2
+ * MODULE RESPONSIBILITY
3
+ * Compile one read-query field clause into SQL.
4
+ */
5
+ import {build_text_expression} from '#src/sql/jsonb/path-parser.js';
6
+ import contains_operator from '#src/sql/jsonb/read/operators/contains.js';
7
+ import {eq_operator, regex_operator} from '#src/sql/read/where/operators/index.js';
8
+ import {compile_base_field_clause, resolve_base_field_name} from '#src/sql/read/where/base-fields.js';
9
+
10
+ import {cast_array_contains_value, cast_query_value} from '#src/sql/read/where/casting.js';
11
+ import {has_operator_entries, should_use_array_contains} from '#src/sql/read/where/context.js';
12
+ import {compile_elem_match_clause} from '#src/sql/jsonb/read/elem-match.js';
13
+ import {compile_operator_object} from '#src/sql/read/where/operators.js';
14
+
15
+ import {build_nested_object} from '#src/utils/object-path.js';
16
+ import {is_object} from '#src/utils/value.js';
17
+
18
+ function compile_field_clause(path_value, comparison_value, compile_context, parameter_state) {
19
+ const data_column_reference = compile_context.data_column_reference;
20
+ const is_array_path = should_use_array_contains(path_value, comparison_value, compile_context);
21
+ const base_field_name = resolve_base_field_name(path_value);
22
+
23
+ if(base_field_name) {
24
+ return compile_base_field_clause(base_field_name, comparison_value, compile_context, parameter_state);
25
+ }
26
+
27
+ if(comparison_value instanceof RegExp) {
28
+ const text_expression = build_text_expression(data_column_reference, path_value);
29
+ return regex_operator(text_expression, comparison_value.source, comparison_value.flags, parameter_state);
30
+ }
31
+
32
+ if(is_object(comparison_value)) {
33
+ if(comparison_value.$elem_match !== undefined) {
34
+ return compile_elem_match_clause(path_value, comparison_value.$elem_match, data_column_reference, parameter_state);
35
+ }
36
+
37
+ if(has_operator_entries(comparison_value)) {
38
+ return compile_operator_object(path_value, comparison_value, compile_context, parameter_state);
39
+ }
40
+
41
+ const nested_object = build_nested_object(path_value, comparison_value);
42
+ return contains_operator(data_column_reference, nested_object, parameter_state);
43
+ }
44
+
45
+ if(is_array_path) {
46
+ const casted_array_value = cast_array_contains_value(path_value, comparison_value, compile_context);
47
+ const nested_object = build_nested_object(path_value, [casted_array_value]);
48
+ return contains_operator(data_column_reference, nested_object, parameter_state);
49
+ }
50
+
51
+ const text_expression = build_text_expression(data_column_reference, path_value);
52
+ const casted_comparison_value = cast_query_value(path_value, comparison_value, compile_context);
53
+ return eq_operator(text_expression, casted_comparison_value, parameter_state);
54
+ }
55
+
56
+ export {
57
+ compile_field_clause
58
+ };