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.
- package/README.md +36 -18
- package/docs/api/connection.md +144 -0
- package/docs/api/delta-tracker.md +106 -0
- package/docs/api/document.md +77 -0
- package/docs/api/field-types.md +329 -0
- package/docs/api/index.md +35 -0
- package/docs/api/model.md +392 -0
- package/docs/api/query-builder.md +81 -0
- package/docs/api/schema.md +204 -0
- package/docs/architecture-flow.md +397 -0
- package/docs/examples.md +495 -218
- package/docs/jsonb-ops.md +171 -0
- package/docs/lifecycle/model-compilation.md +111 -0
- package/docs/lifecycle.md +146 -0
- package/docs/query-translation.md +11 -10
- package/package.json +10 -3
- package/src/connection/connect.js +12 -17
- package/src/connection/connection.js +128 -0
- package/src/connection/server-capabilities.js +60 -59
- package/src/constants/defaults.js +32 -19
- package/src/constants/{id-strategies.js → id-strategy.js} +28 -29
- package/src/constants/intake-mode.js +8 -0
- package/src/debug/debug-logger.js +17 -15
- package/src/errors/model-overwrite-error.js +25 -0
- package/src/errors/query-error.js +25 -23
- package/src/errors/validation-error.js +25 -23
- package/src/field-types/base-field-type.js +137 -140
- package/src/field-types/builtins/advanced.js +365 -365
- package/src/field-types/builtins/index.js +579 -585
- package/src/field-types/field-type-namespace.js +9 -0
- package/src/field-types/registry.js +149 -122
- package/src/index.js +26 -36
- package/src/migration/ensure-index.js +157 -154
- package/src/migration/ensure-schema.js +27 -15
- package/src/migration/ensure-table.js +44 -31
- package/src/migration/schema-indexes-resolver.js +8 -6
- package/src/model/document-instance.js +29 -540
- package/src/model/document.js +60 -0
- package/src/model/factory/constants.js +36 -0
- package/src/model/factory/index.js +58 -0
- package/src/model/model.js +875 -0
- package/src/model/operations/delete-one.js +39 -0
- package/src/model/operations/insert-one.js +35 -0
- package/src/model/operations/query-builder.js +132 -0
- package/src/model/operations/update-one.js +333 -0
- package/src/model/state.js +34 -0
- package/src/schema/field-definition-parser.js +213 -218
- package/src/schema/path-introspection.js +87 -82
- package/src/schema/schema-compiler.js +126 -212
- package/src/schema/schema.js +621 -138
- package/src/sql/index.js +17 -0
- package/src/sql/jsonb/ops.js +153 -0
- package/src/{query → sql/jsonb}/path-parser.js +54 -43
- package/src/sql/jsonb/read/elem-match.js +133 -0
- package/src/{query → sql/jsonb/read}/operators/contains.js +13 -7
- package/src/sql/jsonb/read/operators/elem-match.js +9 -0
- package/src/{query → sql/jsonb/read}/operators/has-all-keys.js +17 -11
- package/src/{query → sql/jsonb/read}/operators/has-any-keys.js +18 -11
- package/src/sql/jsonb/read/operators/has-key.js +12 -0
- package/src/{query → sql/jsonb/read}/operators/jsonpath-exists.js +22 -15
- package/src/{query → sql/jsonb/read}/operators/jsonpath-match.js +22 -15
- package/src/{query → sql/jsonb/read}/operators/size.js +23 -16
- package/src/sql/parameter-binder.js +18 -13
- package/src/sql/read/build-count-query.js +12 -0
- package/src/sql/read/build-find-query.js +25 -0
- package/src/sql/read/limit-skip.js +21 -0
- package/src/sql/read/sort.js +85 -0
- package/src/sql/read/where/base-fields.js +310 -0
- package/src/sql/read/where/casting.js +90 -0
- package/src/sql/read/where/context.js +79 -0
- package/src/sql/read/where/field-clause.js +58 -0
- package/src/sql/read/where/index.js +38 -0
- package/src/sql/read/where/operator-entries.js +29 -0
- package/src/{query → sql/read/where}/operators/all.js +16 -10
- package/src/sql/read/where/operators/eq.js +12 -0
- package/src/{query → sql/read/where}/operators/gt.js +23 -16
- package/src/{query → sql/read/where}/operators/gte.js +23 -16
- package/src/{query → sql/read/where}/operators/in.js +18 -12
- package/src/sql/read/where/operators/index.js +40 -0
- package/src/{query → sql/read/where}/operators/lt.js +23 -16
- package/src/{query → sql/read/where}/operators/lte.js +23 -16
- package/src/sql/read/where/operators/ne.js +12 -0
- package/src/{query → sql/read/where}/operators/nin.js +18 -12
- package/src/{query → sql/read/where}/operators/regex.js +14 -8
- package/src/sql/read/where/operators.js +126 -0
- package/src/sql/read/where/text-operators.js +83 -0
- package/src/sql/run.js +46 -0
- package/src/sql/write/build-delete-query.js +33 -0
- package/src/sql/write/build-insert-query.js +42 -0
- package/src/sql/write/build-update-query.js +65 -0
- package/src/utils/assert.js +34 -27
- package/src/utils/delta-tracker/.archive/1 tracker-redesign-codex-v2.md +250 -0
- package/src/utils/delta-tracker/.archive/1 tracker-redesign-gemini.md +101 -0
- package/src/utils/delta-tracker/.archive/2 evaluation by gemini.txt +65 -0
- package/src/utils/delta-tracker/.archive/2 evaluation by grok.txt +39 -0
- package/src/utils/delta-tracker/.archive/3 gemini evaluate grok.txt +37 -0
- package/src/utils/delta-tracker/.archive/3 grok evaluate gemini.txt +63 -0
- package/src/utils/delta-tracker/.archive/4 gemini veredict.txt +16 -0
- package/src/utils/delta-tracker/.archive/index.1.js +587 -0
- package/src/utils/delta-tracker/.archive/index.2.js +612 -0
- package/src/utils/delta-tracker/index.js +592 -0
- package/src/utils/dirty-tracker/inline.js +335 -0
- package/src/utils/dirty-tracker/instance.js +414 -0
- package/src/utils/dirty-tracker/static.js +343 -0
- package/src/utils/json-safe.js +13 -9
- package/src/utils/object-path.js +227 -33
- package/src/utils/object.js +408 -168
- package/src/utils/string.js +55 -0
- package/src/utils/value.js +169 -30
- package/docs/api.md +0 -152
- package/src/connection/disconnect.js +0 -16
- package/src/connection/pool-store.js +0 -46
- package/src/model/model-factory.js +0 -555
- package/src/query/limit-skip-compiler.js +0 -31
- package/src/query/operators/elem-match.js +0 -3
- package/src/query/operators/eq.js +0 -6
- package/src/query/operators/has-key.js +0 -6
- package/src/query/operators/index.js +0 -60
- package/src/query/operators/ne.js +0 -6
- package/src/query/query-builder.js +0 -93
- package/src/query/sort-compiler.js +0 -30
- package/src/query/where-compiler.js +0 -477
- 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
|
+
};
|