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,9 @@
1
+ import {register, resolve} from '#src/field-types/registry.js';
2
+
3
+ const field_type = Object.freeze({
4
+ register,
5
+ resolve
6
+ });
7
+
8
+ export default field_type;
9
+ export {field_type};
@@ -1,122 +1,149 @@
1
- /*
2
- Assumptions and trade-offs:
3
- - A process-level default registry is used for built-ins and extension registration.
4
- - Type resolution is explicit; unsupported references throw immediately.
5
- */
6
- import {get_foundational_field_types} from '#src/field-types/builtins/index.js';
7
-
8
- function FieldTypeRegistry() {
9
- this.type_constructors_by_name = Object.create(null);
10
- this.type_name_by_reference = new Map();
11
- this.register_foundational_types();
12
- }
13
-
14
- FieldTypeRegistry.prototype.register_foundational_types = function () {
15
- const foundational_type_entries = get_foundational_type_entries();
16
- let entry_index = 0;
17
-
18
- while(entry_index < foundational_type_entries.length) {
19
- const type_entry = foundational_type_entries[entry_index];
20
- const type_name = type_entry[0];
21
- const type_definition = type_entry[1];
22
-
23
- this.register_field_type(type_name, type_definition.constructor, type_definition.references);
24
- entry_index += 1;
25
- }
26
- };
27
-
28
- FieldTypeRegistry.prototype.register_field_type = function (type_name, type_constructor, references) {
29
- const resolved_name = resolve_type_name_value(type_name);
30
- const resolved_references = resolve_reference_list(references);
31
-
32
- this.type_constructors_by_name[resolved_name] = type_constructor;
33
- this.type_name_by_reference.set(resolved_name, resolved_name);
34
- register_reference_aliases(this.type_name_by_reference, resolved_name, resolved_references);
35
- };
36
-
37
- FieldTypeRegistry.prototype.resolve_field_type_name = function (type_reference) {
38
- return resolve_field_type_name_value(
39
- this.type_name_by_reference,
40
- this.type_constructors_by_name,
41
- type_reference
42
- );
43
- };
44
-
45
- FieldTypeRegistry.prototype.has_field_type = function (type_reference) {
46
- const type_name = this.resolve_field_type_name(type_reference);
47
- return type_name !== null;
48
- };
49
-
50
- FieldTypeRegistry.prototype.create_field_type = function (path_value, type_reference, options) {
51
- const type_name = this.resolve_field_type_name(type_reference);
52
- assert_supported_type_name(path_value, type_name);
53
-
54
- const type_constructor = this.type_constructors_by_name[type_name];
55
- const field_options = create_field_options(options);
56
-
57
- return new type_constructor(path_value, field_options, this);
58
- };
59
-
60
- function get_foundational_type_entries() {
61
- const foundational_types = get_foundational_field_types();
62
- return Object.entries(foundational_types);
63
- }
64
-
65
- function resolve_type_name_value(type_name) {
66
- return String(type_name);
67
- }
68
-
69
- function resolve_reference_list(references) {
70
- if(Array.isArray(references)) {
71
- return references;
72
- }
73
-
74
- return [];
75
- }
76
-
77
- function register_reference_aliases(type_name_by_reference, resolved_name, resolved_references) {
78
- let reference_index = 0;
79
-
80
- while(reference_index < resolved_references.length) {
81
- type_name_by_reference.set(resolved_references[reference_index], resolved_name);
82
- reference_index += 1;
83
- }
84
- }
85
-
86
- function resolve_field_type_name_value(type_name_by_reference, type_constructors_by_name, type_reference) {
87
- if(type_name_by_reference.has(type_reference)) {
88
- return type_name_by_reference.get(type_reference);
89
- }
90
-
91
- if(typeof type_reference === 'string' && type_constructors_by_name[type_reference]) {
92
- return type_reference;
93
- }
94
-
95
- return null;
96
- }
97
-
98
- function create_field_options(options) {
99
- return Object.assign({}, options || {});
100
- }
101
-
102
- function assert_supported_type_name(path_value, type_name) {
103
- if(!type_name) {
104
- throw new Error('Unsupported field type at path "' + path_value + '"');
105
- }
106
- }
107
-
108
- const default_field_type_registry = new FieldTypeRegistry();
109
-
110
- function register_field_type(type_name, type_constructor, references) {
111
- default_field_type_registry.register_field_type(type_name, type_constructor, references);
112
- }
113
-
114
- function resolve_field_type(type_reference) {
115
- return default_field_type_registry.resolve_field_type_name(type_reference);
116
- }
117
-
118
- function create_field_type(path_value, type_reference, options) {
119
- return default_field_type_registry.create_field_type(path_value, type_reference, options);
120
- }
121
-
122
- export {create_field_type, default_field_type_registry, FieldTypeRegistry, register_field_type, resolve_field_type};
1
+ import {get_foundational_field_types} from '#src/field-types/builtins/index.js';
2
+ import {deep_clone} from '#src/utils/object.js';
3
+
4
+ function FieldTypeRegistry() {
5
+ this.type_constructors_by_name = Object.create(null);
6
+ this.type_name_by_alias = new Map();
7
+ this.register_foundational_types();
8
+ }
9
+
10
+ /**
11
+ * Registers the foundational field-type entries in the default registry format.
12
+ *
13
+ * @returns {void}
14
+ */
15
+ FieldTypeRegistry.prototype.register_foundational_types = function () {
16
+ const foundational_type_entries = Object.entries(get_foundational_field_types());
17
+ let entry_index = 0;
18
+
19
+ while(entry_index < foundational_type_entries.length) {
20
+ const type_entry = foundational_type_entries[entry_index];
21
+ const type_name = type_entry[0];
22
+ const type_definition = type_entry[1];
23
+
24
+ this.register(type_name, type_definition.constructor, type_definition.aliases);
25
+ entry_index += 1;
26
+ }
27
+ };
28
+
29
+ /**
30
+ * Registers a field type and its lookup aliases.
31
+ *
32
+ * @param {string} type_name Canonical field-type name.
33
+ * @param {Function} type_constructor Field-type constructor.
34
+ * @param {Array<*>} aliases Optional alias list used for lookup resolution.
35
+ * @returns {void}
36
+ */
37
+ FieldTypeRegistry.prototype.register = function (type_name, type_constructor, aliases) {
38
+ const resolved_name = String(type_name);
39
+ const resolved_aliases = Array.isArray(aliases) ? aliases : [];
40
+
41
+ this.type_constructors_by_name[resolved_name] = type_constructor;
42
+ this.type_name_by_alias.set(resolved_name, resolved_name);
43
+
44
+ register_aliases(this.type_name_by_alias, resolved_name, resolved_aliases);
45
+ };
46
+
47
+ /**
48
+ * Resolves an alias or canonical name to the registered field-type name.
49
+ *
50
+ * @param {*} type_alias Alias or canonical field-type reference.
51
+ * @returns {string|null}
52
+ */
53
+ FieldTypeRegistry.prototype.resolve = function (type_alias) {
54
+ if(this.type_name_by_alias.has(type_alias)) {
55
+ return this.type_name_by_alias.get(type_alias);
56
+ }
57
+
58
+ if(typeof type_alias === 'string' && this.type_constructors_by_name[type_alias]) {
59
+ return type_alias;
60
+ }
61
+
62
+ return null;
63
+ };
64
+
65
+ /**
66
+ * Checks whether a field type exists in the registry.
67
+ *
68
+ * @param {*} type_alias Alias or canonical field-type reference.
69
+ * @returns {boolean}
70
+ */
71
+ FieldTypeRegistry.prototype.has_field_type = function (type_alias) {
72
+ const type_name = this.resolve(type_alias);
73
+ return type_name !== null;
74
+ };
75
+
76
+ /**
77
+ * Creates a field-type instance for a schema path.
78
+ *
79
+ * @param {string} path_value Schema path.
80
+ * @param {*} type_alias Alias or canonical field-type reference.
81
+ * @param {object} options Field options.
82
+ * @returns {object}
83
+ * @throws {Error} When the field type is unsupported.
84
+ */
85
+ FieldTypeRegistry.prototype.create = function (path_value, type_alias, options) {
86
+ const type_name = this.resolve(type_alias);
87
+
88
+ if(!type_name) {
89
+ throw new Error('Unsupported field type at path "' + path_value + '"');
90
+ }
91
+
92
+ const type_constructor = this.type_constructors_by_name[type_name];
93
+ const field_options = create_field_options(options);
94
+
95
+ return new type_constructor(path_value, field_options, this);
96
+ };
97
+
98
+ /**
99
+ * Stores aliases in the lookup map for a canonical field-type name.
100
+ *
101
+ * @param {Map<*, string>} type_name_by_alias Alias-to-name lookup map.
102
+ * @param {string} resolved_name Canonical field-type name.
103
+ * @param {Array<*>} resolved_aliases Normalized alias list.
104
+ * @returns {void}
105
+ */
106
+ function register_aliases(type_name_by_alias, resolved_name, resolved_aliases) {
107
+ let alias_index = 0;
108
+
109
+ // Resolve aliases once at registration time so later lookups stay canonical.
110
+ while(alias_index < resolved_aliases.length) {
111
+ type_name_by_alias.set(resolved_aliases[alias_index], resolved_name);
112
+ alias_index += 1;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Clones plain field options without flattening live FieldType instances.
118
+ *
119
+ * @param {object} options Field options.
120
+ * @returns {object}
121
+ */
122
+ function create_field_options(options) {
123
+ // Bugfix note:
124
+ // Parser-generated options can include nested FieldType instances (`of_field_type`, `of_field_types`).
125
+ // Those must keep their prototype methods intact, so the clone helper preserves custom instances by reference.
126
+ return deep_clone(options || {});
127
+ }
128
+
129
+ const default_field_type_registry = new FieldTypeRegistry();
130
+
131
+ function register(type_name, type_constructor, aliases) {
132
+ default_field_type_registry.register(type_name, type_constructor, aliases);
133
+ }
134
+
135
+ function resolve(type_alias) {
136
+ return default_field_type_registry.resolve(type_alias);
137
+ }
138
+
139
+ function create(path_value, type_alias, options) {
140
+ return default_field_type_registry.create(path_value, type_alias, options);
141
+ }
142
+
143
+ export {
144
+ create,
145
+ default_field_type_registry,
146
+ FieldTypeRegistry,
147
+ register,
148
+ resolve
149
+ };
package/src/index.js CHANGED
@@ -1,36 +1,26 @@
1
- import connect from '#src/connection/connect.js';
2
- import disconnect from '#src/connection/disconnect.js';
3
- import IdStrategies from '#src/constants/id-strategies.js';
4
- import Schema from '#src/schema/schema.js';
5
- import model from '#src/model/model-factory.js';
6
- import {create_field_type, register_field_type, resolve_field_type} from '#src/field-types/registry.js';
7
- import {boolean_convert_to_false, boolean_convert_to_true, uuidv7_type_reference} from '#src/field-types/builtins/index.js';
8
-
9
- const jsonbadger = {
10
- connect,
11
- disconnect,
12
- model,
13
- create_field_type,
14
- register_field_type,
15
- resolve_field_type,
16
- Schema,
17
- IdStrategies,
18
- field_types: {
19
- UUIDv7: uuidv7_type_reference,
20
- boolean_convert_to_true,
21
- boolean_convert_to_false
22
- }
23
- };
24
-
25
- export {
26
- connect,
27
- disconnect,
28
- model,
29
- create_field_type,
30
- register_field_type,
31
- resolve_field_type,
32
- Schema,
33
- IdStrategies
34
- };
35
-
36
- export default jsonbadger;
1
+ import connect from '#src/connection/connect.js';
2
+ import ID_STRATEGY from '#src/constants/id-strategy.js';
3
+ import Schema from '#src/schema/schema.js';
4
+ import field_type from '#src/field-types/field-type-namespace.js';
5
+ import {boolean_convert_to_false, boolean_convert_to_true, uuidv7_type_reference} from '#src/field-types/builtins/index.js';
6
+
7
+ const jsonbadger = {
8
+ ID_STRATEGY,
9
+ connect,
10
+ field_type,
11
+ Schema,
12
+ field_types: {
13
+ UUIDv7: uuidv7_type_reference,
14
+ boolean_convert_to_true,
15
+ boolean_convert_to_false
16
+ }
17
+ };
18
+
19
+ export {
20
+ ID_STRATEGY,
21
+ connect,
22
+ field_type,
23
+ Schema
24
+ };
25
+
26
+ export default jsonbadger;
@@ -1,155 +1,158 @@
1
- import {assert_condition, assert_identifier, assert_path, quote_identifier} from '#src/utils/assert.js';
2
- import {build_path_literal, split_dot_path} from '#src/utils/object-path.js';
3
- import sql_runner from '#src/sql/sql-runner.js';
4
- import {is_object, is_string} from '#src/utils/value.js';
5
-
6
- export default async function ensure_index(table_name, index_spec, data_column, index_options) {
7
- assert_identifier(table_name, 'table_name');
8
- assert_identifier(data_column, 'data_column');
9
-
10
- const normalized_index_spec = normalize_index_spec(index_spec);
11
- const normalized_index_options = normalize_index_options(index_options);
12
-
13
- const table_identifier = quote_identifier(table_name);
14
- const data_identifier = quote_identifier(data_column);
15
- const index_name = resolve_index_name(table_name, data_column, normalized_index_spec, normalized_index_options);
16
- const index_identifier = quote_identifier(index_name);
17
-
18
- if(is_string(normalized_index_spec)) {
19
- assert_condition(
20
- normalized_index_options.unique !== true,
21
- 'unique index is not supported for single-path GIN indexes'
22
- );
23
-
24
- const expression = build_json_path_expression(data_identifier, normalized_index_spec);
25
- const sql_text =
26
- 'CREATE INDEX IF NOT EXISTS ' + index_identifier +
27
- ' ON ' + table_identifier + ' USING GIN ((' + expression + '));';
28
-
29
- await sql_runner(sql_text, []);
30
- return;
31
- }
32
-
33
- const sorted_path_entries = Object.entries(normalized_index_spec);
34
- const expression_list = [];
35
- let entry_position = 0;
36
-
37
- while(entry_position < sorted_path_entries.length) {
38
- const path_entry = sorted_path_entries[entry_position];
39
- const path_value = path_entry[0];
40
- const direction_value = path_entry[1];
41
- const direction_sql = direction_value === -1 ? 'DESC' : 'ASC';
42
- const expression = build_text_path_expression(data_identifier, path_value);
43
-
44
- expression_list.push('(' + expression + ') ' + direction_sql);
45
- entry_position += 1;
46
- }
47
-
48
- const unique_prefix = normalized_index_options.unique ? 'UNIQUE ' : '';
49
- const sql_text =
50
- 'CREATE ' + unique_prefix + 'INDEX IF NOT EXISTS ' + index_identifier +
51
- ' ON ' + table_identifier + ' (' + expression_list.join(', ') + ');';
52
-
53
- await sql_runner(sql_text, []);
54
- }
55
-
56
- function normalize_index_spec(index_spec) {
57
- if(is_string(index_spec)) {
58
- assert_path(index_spec, 'index path');
59
- return index_spec;
60
- }
61
-
62
- assert_condition(is_object(index_spec), 'index_spec must be a path string or an object map of paths to sort directions');
63
-
64
- const index_entries = Object.entries(index_spec);
65
- assert_condition(index_entries.length > 0, 'index_spec object must define at least one indexed path');
66
-
67
- const normalized_index_spec = {};
68
- let entry_position = 0;
69
-
70
- while(entry_position < index_entries.length) {
71
- const index_entry = index_entries[entry_position];
72
- const path_value = index_entry[0];
73
- const direction_value = index_entry[1];
74
-
75
- assert_path(path_value, 'index path');
76
- assert_condition(direction_value === 1 || direction_value === -1, 'index direction for path "' + path_value + '" must be 1 or -1');
77
-
78
- normalized_index_spec[path_value] = direction_value;
79
- entry_position += 1;
80
- }
81
-
82
- return normalized_index_spec;
83
- }
84
-
85
- function normalize_index_options(index_options) {
86
- if(index_options === undefined) {
87
- return {};
88
- }
89
-
90
- assert_condition(is_object(index_options), 'index_options must be an object');
91
-
92
- const normalized_options = Object.assign({}, index_options);
93
-
94
- if(normalized_options.unique !== undefined) {
95
- assert_condition(typeof normalized_options.unique === 'boolean', 'index_options.unique must be a boolean');
96
- }
97
-
98
- if(normalized_options.name !== undefined) {
99
- assert_identifier(normalized_options.name, 'index_options.name');
100
- }
101
-
102
- return normalized_options;
103
- }
104
-
105
- function resolve_index_name(table_name, data_column, index_spec, index_options) {
106
- if(index_options.name) {
107
- return index_options.name;
108
- }
109
-
110
- if(is_string(index_spec)) {
111
- const path_suffix = index_spec.replace(/\./g, '_');
112
- return build_index_name(table_name, data_column, path_suffix + '_gin');
113
- }
114
-
115
- const index_entries = Object.entries(index_spec);
116
- const path_suffix_parts = [];
117
- let entry_position = 0;
118
-
119
- while(entry_position < index_entries.length) {
120
- const index_entry = index_entries[entry_position];
121
- const path_value = index_entry[0];
122
- const direction_value = index_entry[1];
123
- const direction_suffix = direction_value === -1 ? 'desc' : 'asc';
124
- path_suffix_parts.push(path_value.replace(/\./g, '_') + '_' + direction_suffix);
125
- entry_position += 1;
126
- }
127
-
128
- return build_index_name(table_name, data_column, path_suffix_parts.join('_') + '_btree');
129
- }
130
-
131
- function build_json_path_expression(data_identifier, path_value) {
132
- const path_segments = split_dot_path(path_value);
133
-
134
- if(path_segments.length === 1) {
135
- return data_identifier + " -> '" + path_segments[0] + "'";
136
- }
137
-
138
- return data_identifier + " #> '" + build_path_literal(path_segments) + "'";
139
- }
140
-
141
- function build_text_path_expression(data_identifier, path_value) {
142
- const path_segments = split_dot_path(path_value);
143
-
144
- if(path_segments.length === 1) {
145
- return data_identifier + " ->> '" + path_segments[0] + "'";
146
- }
147
-
148
- return data_identifier + " #>> '" + build_path_literal(path_segments) + "'";
149
- }
150
-
151
- function build_index_name(table_name, data_column, path_suffix) {
152
- const raw_name = 'idx_' + table_name + '_' + data_column + '_' + path_suffix;
153
- const normalized_name = raw_name.toLowerCase().replace(/[^a-z0-9_]/g, '_');
154
- return normalized_name.slice(0, 63);
1
+ /*
2
+ * MODULE RESPONSIBILITY
3
+ * Ensure the declared model indexes exist in the backing database.
4
+ */
5
+ import {assert_condition, assert_identifier, assert_path, quote_identifier} from '#src/utils/assert.js';
6
+ import {build_path_literal, split_dot_path} from '#src/utils/object-path.js';
7
+ import {is_object, is_string} from '#src/utils/value.js';
8
+ import run from '#src/sql/run.js';
9
+
10
+ async function ensure_index(context) {
11
+ const {
12
+ table_name,
13
+ index_definition,
14
+ data_column,
15
+ connection
16
+ } = context;
17
+
18
+ assert_identifier(table_name, 'table_name');
19
+ assert_identifier(data_column, 'data_column');
20
+ assert_sql_safe_index_definition(index_definition);
21
+
22
+ const table_identifier = quote_identifier(table_name);
23
+ const data_identifier = quote_identifier(data_column);
24
+ const index_name = resolve_index_name(table_name, data_column, index_definition);
25
+ const index_identifier = quote_identifier(index_name);
26
+
27
+ if(index_definition.using === 'gin') {
28
+ const expression = build_json_path_expression(data_identifier, index_definition.path);
29
+ const sql_text =
30
+ `CREATE INDEX IF NOT EXISTS ${index_identifier} ` +
31
+ `ON ${table_identifier} USING GIN ((${expression}));`;
32
+
33
+ await run(sql_text, [], connection);
34
+ return;
35
+ }
36
+
37
+ const btree_path_order_map = build_btree_path_order_map(index_definition);
38
+ const sorted_path_entries = Object.entries(btree_path_order_map);
39
+ const expression_list = [];
40
+ let entry_position = 0;
41
+
42
+ while(entry_position < sorted_path_entries.length) {
43
+ const path_entry = sorted_path_entries[entry_position];
44
+ const path_value = path_entry[0];
45
+ const order_value = path_entry[1];
46
+ const order_sql = order_value === -1 ? 'DESC' : 'ASC';
47
+ const expression = build_text_path_expression(data_identifier, path_value);
48
+
49
+ expression_list.push('(' + expression + ') ' + order_sql);
50
+ entry_position += 1;
51
+ }
52
+
53
+ const unique_prefix = index_definition.unique ? 'UNIQUE ' : '';
54
+ const sql_text =
55
+ `CREATE ${unique_prefix}INDEX IF NOT EXISTS ${index_identifier} ` +
56
+ `ON ${table_identifier} (${expression_list.join(', ')});`;
57
+
58
+ await run(sql_text, [], connection);
155
59
  }
60
+
61
+ function assert_sql_safe_index_definition(index_definition) {
62
+ assert_condition(is_object(index_definition), 'index_definition must be an object');
63
+ assert_condition(index_definition.using === 'gin' || index_definition.using === 'btree', 'index_definition.using must be "gin" or "btree"');
64
+
65
+ if(index_definition.name !== undefined) {
66
+ assert_identifier(index_definition.name, 'index_definition.name');
67
+ }
68
+
69
+ if(index_definition.using === 'gin') {
70
+ assert_condition(is_string(index_definition.path), 'index_definition.path must be a string when using is "gin"');
71
+ assert_path(index_definition.path, 'index path');
72
+ return;
73
+ }
74
+
75
+ const has_single_path = is_string(index_definition.path);
76
+ const has_multiple_paths = is_object(index_definition.paths) && Object.keys(index_definition.paths).length > 0;
77
+
78
+ assert_condition(has_single_path || has_multiple_paths, 'index_definition.path or index_definition.paths is required when using is "btree"');
79
+
80
+ if(has_single_path) {
81
+ assert_path(index_definition.path, 'index path');
82
+ }
83
+
84
+ if(has_multiple_paths) {
85
+ for(const path_value of Object.keys(index_definition.paths)) {
86
+ assert_path(path_value, 'index path');
87
+ }
88
+ }
89
+ }
90
+
91
+ function resolve_index_name(table_name, data_column, index_definition) {
92
+ if(index_definition.name) {
93
+ return index_definition.name;
94
+ }
95
+
96
+ if(index_definition.using === 'gin') {
97
+ const path_suffix = index_definition.path.replace(/\./g, '_');
98
+ return build_index_name(table_name, data_column, path_suffix + '_gin');
99
+ }
100
+
101
+ const btree_path_order_map = build_btree_path_order_map(index_definition);
102
+ const index_entries = Object.entries(btree_path_order_map);
103
+ const path_suffix_parts = [];
104
+ let entry_position = 0;
105
+
106
+ while(entry_position < index_entries.length) {
107
+ const index_entry = index_entries[entry_position];
108
+ const path_value = index_entry[0];
109
+ const order_value = index_entry[1];
110
+ const order_suffix = order_value === -1 ? 'desc' : 'asc';
111
+ path_suffix_parts.push(path_value.replace(/\./g, '_') + '_' + order_suffix);
112
+ entry_position += 1;
113
+ }
114
+
115
+ return build_index_name(table_name, data_column, path_suffix_parts.join('_') + '_btree');
116
+ }
117
+
118
+ function build_btree_path_order_map(index_definition) {
119
+ if(is_object(index_definition.paths)) {
120
+ const path_entries = Object.entries(index_definition.paths);
121
+
122
+ if(path_entries.length > 0) {
123
+ return Object.assign({}, index_definition.paths);
124
+ }
125
+ }
126
+
127
+ return {
128
+ [index_definition.path]: index_definition.order
129
+ };
130
+ }
131
+
132
+ function build_json_path_expression(data_identifier, path_value) {
133
+ const path_segments = split_dot_path(path_value);
134
+
135
+ if(path_segments.length === 1) {
136
+ return `${data_identifier} -> '${path_segments[0]}'`;
137
+ }
138
+
139
+ return `${data_identifier} #> '${build_path_literal(path_segments)}'`;
140
+ }
141
+
142
+ function build_text_path_expression(data_identifier, path_value) {
143
+ const path_segments = split_dot_path(path_value);
144
+
145
+ if(path_segments.length === 1) {
146
+ return `${data_identifier} ->> '${path_segments[0]}'`;
147
+ }
148
+
149
+ return `${data_identifier} #>> '${build_path_literal(path_segments)}'`;
150
+ }
151
+
152
+ function build_index_name(table_name, data_column, path_suffix) {
153
+ const raw_name = `idx_${table_name}_${data_column}_${path_suffix}`;
154
+ const normalized_name = raw_name.toLowerCase().replace(/[^a-z0-9_]/g, '_');
155
+ return normalized_name.slice(0, 63);
156
+ }
157
+
158
+ export default ensure_index;