Skip to content

Commit dd4086a

Browse files
author
bosd
committed
[IMP] base_sparse_field_jsonb: tests
1 parent 5c47e95 commit dd4086a

File tree

1 file changed

+358
-0
lines changed

1 file changed

+358
-0
lines changed

base_sparse_field_jsonb/tests/test_sparse_fields_jsonb.py

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@
77
https://github.com/odoo/odoo/blob/19.0/addons/base_sparse_field/tests/test_sparse_fields.py
88
"""
99

10+
import json
11+
1012
from odoo import fields, models
1113
from odoo.orm.model_classes import add_to_registry
1214
from odoo.tests import TransactionCase
1315

16+
from ..hooks import drop_gin_indexes_on_text_columns, post_init_hook, pre_init_hook
17+
from ..models.fields import SerializedJsonb
18+
1419

1520
class SparseFieldsTestModel(models.Model):
1621
"""Test model for sparse fields with JSONB storage."""
@@ -216,3 +221,356 @@ def test_selection_sparse_field(self):
216221
# Clear selection
217222
record.write({"selection": False})
218223
self.assertFalse(record.selection)
224+
225+
def test_convert_to_cache_with_dict(self):
226+
"""Test convert_to_cache with dict value returns JSON string."""
227+
field = SerializedJsonb()
228+
record = self.env["sparse_fields_jsonb.test"].create({})
229+
230+
# Dict should be converted to JSON string
231+
result = field.convert_to_cache({"key": "value"}, record)
232+
self.assertEqual(result, '{"key": "value"}')
233+
234+
# Nested dict
235+
nested = {"level1": {"level2": {"level3": "deep"}}}
236+
result = field.convert_to_cache(nested, record)
237+
self.assertEqual(json.loads(result), nested)
238+
239+
def test_convert_to_cache_with_non_dict(self):
240+
"""Test convert_to_cache with non-dict values."""
241+
field = SerializedJsonb()
242+
record = self.env["sparse_fields_jsonb.test"].create({})
243+
244+
# None should return None
245+
result = field.convert_to_cache(None, record)
246+
self.assertIsNone(result)
247+
248+
# Empty string should return None
249+
result = field.convert_to_cache("", record)
250+
self.assertIsNone(result)
251+
252+
# False should return None
253+
result = field.convert_to_cache(False, record)
254+
self.assertIsNone(result)
255+
256+
# JSON string should pass through
257+
json_str = '{"existing": "json"}'
258+
result = field.convert_to_cache(json_str, record)
259+
self.assertEqual(result, json_str)
260+
261+
def test_convert_to_record_with_dict(self):
262+
"""Test convert_to_record with dict value (from JSONB)."""
263+
field = SerializedJsonb()
264+
record = self.env["sparse_fields_jsonb.test"].create({})
265+
266+
# Dict should pass through unchanged
267+
data = {"key": "value", "number": 42}
268+
result = field.convert_to_record(data, record)
269+
self.assertEqual(result, data)
270+
271+
def test_convert_to_record_with_none(self):
272+
"""Test convert_to_record with None returns empty dict."""
273+
field = SerializedJsonb()
274+
record = self.env["sparse_fields_jsonb.test"].create({})
275+
276+
result = field.convert_to_record(None, record)
277+
self.assertEqual(result, {})
278+
279+
def test_convert_to_record_with_string(self):
280+
"""Test convert_to_record with string value (fallback for TEXT)."""
281+
field = SerializedJsonb()
282+
record = self.env["sparse_fields_jsonb.test"].create({})
283+
284+
# JSON string should be parsed
285+
json_str = '{"from": "string"}'
286+
result = field.convert_to_record(json_str, record)
287+
self.assertEqual(result, {"from": "string"})
288+
289+
# Empty string should return empty dict
290+
result = field.convert_to_record("", record)
291+
self.assertEqual(result, {})
292+
293+
def test_convert_to_column_insert_with_none(self):
294+
"""Test convert_to_column_insert with None value."""
295+
field = SerializedJsonb()
296+
record = self.env["sparse_fields_jsonb.test"].create({})
297+
298+
result = field.convert_to_column_insert(None, record)
299+
self.assertIsNone(result)
300+
301+
def test_convert_to_column_update_with_none(self):
302+
"""Test convert_to_column_update with None value."""
303+
field = SerializedJsonb()
304+
record = self.env["sparse_fields_jsonb.test"].create({})
305+
306+
result = field.convert_to_column_update(None, record)
307+
self.assertIsNone(result)
308+
309+
def test_postgresql_json_containment_operator(self):
310+
"""Test PostgreSQL @> containment operator on JSONB."""
311+
record = self.env["sparse_fields_jsonb.test"].create(
312+
{
313+
"boolean": True,
314+
"integer": 42,
315+
"char": "test",
316+
}
317+
)
318+
record.flush_recordset()
319+
320+
# Test @> containment operator
321+
self.env.cr.execute(
322+
"""
323+
SELECT id FROM sparse_fields_jsonb_test
324+
WHERE data @> %s::jsonb
325+
""",
326+
('{"integer": 42}',),
327+
)
328+
result = self.env.cr.fetchone()
329+
self.assertIsNotNone(result)
330+
self.assertEqual(result[0], record.id)
331+
332+
def test_postgresql_json_key_exists_operator(self):
333+
"""Test PostgreSQL ? key exists operator on JSONB."""
334+
record = self.env["sparse_fields_jsonb.test"].create(
335+
{
336+
"char": "has_char",
337+
}
338+
)
339+
record.flush_recordset()
340+
341+
# Test ? key exists operator
342+
self.env.cr.execute(
343+
"""
344+
SELECT id FROM sparse_fields_jsonb_test
345+
WHERE data ? 'char'
346+
"""
347+
)
348+
results = self.env.cr.fetchall()
349+
record_ids = [r[0] for r in results]
350+
self.assertIn(record.id, record_ids)
351+
352+
def test_postgresql_json_path_operator(self):
353+
"""Test PostgreSQL -> and ->> path operators on JSONB."""
354+
record = self.env["sparse_fields_jsonb.test"].create(
355+
{
356+
"integer": 999,
357+
"char": "path_test",
358+
}
359+
)
360+
record.flush_recordset()
361+
362+
# Test -> operator (returns jsonb)
363+
self.env.cr.execute(
364+
"""
365+
SELECT data->'integer' FROM sparse_fields_jsonb_test
366+
WHERE id = %s
367+
""",
368+
(record.id,),
369+
)
370+
result = self.env.cr.fetchone()
371+
self.assertEqual(result[0], 999)
372+
373+
# Test ->> operator (returns text)
374+
self.env.cr.execute(
375+
"""
376+
SELECT data->>'char' FROM sparse_fields_jsonb_test
377+
WHERE id = %s
378+
""",
379+
(record.id,),
380+
)
381+
result = self.env.cr.fetchone()
382+
self.assertEqual(result[0], "path_test")
383+
384+
def test_batch_create_multiple_records(self):
385+
"""Test creating multiple records with sparse fields."""
386+
records = self.env["sparse_fields_jsonb.test"].create(
387+
[
388+
{"integer": 1, "char": "first"},
389+
{"integer": 2, "char": "second"},
390+
{"integer": 3, "char": "third"},
391+
]
392+
)
393+
394+
self.assertEqual(len(records), 3)
395+
self.assertEqual(records[0].integer, 1)
396+
self.assertEqual(records[1].integer, 2)
397+
self.assertEqual(records[2].integer, 3)
398+
399+
def test_batch_write_multiple_records(self):
400+
"""Test writing to multiple records with sparse fields."""
401+
records = self.env["sparse_fields_jsonb.test"].create(
402+
[
403+
{"integer": 1},
404+
{"integer": 2},
405+
{"integer": 3},
406+
]
407+
)
408+
409+
# Update all records at once
410+
records.write({"char": "batch_updated"})
411+
412+
for record in records:
413+
self.assertEqual(record.char, "batch_updated")
414+
415+
def test_search_with_sparse_fields(self):
416+
"""Test that records can be searched after sparse field operations."""
417+
record = self.env["sparse_fields_jsonb.test"].create({"char": "searchable"})
418+
419+
# Search should work
420+
found = self.env["sparse_fields_jsonb.test"].search([("id", "=", record.id)])
421+
self.assertEqual(len(found), 1)
422+
self.assertEqual(found.char, "searchable")
423+
424+
def test_copy_record_with_sparse_fields(self):
425+
"""Test copying a record preserves sparse field values."""
426+
original = self.env["sparse_fields_jsonb.test"].create(
427+
{
428+
"boolean": True,
429+
"integer": 100,
430+
"char": "original",
431+
}
432+
)
433+
434+
copy = original.copy()
435+
436+
self.assertEqual(copy.boolean, True)
437+
self.assertEqual(copy.integer, 100)
438+
self.assertEqual(copy.char, "original")
439+
self.assertNotEqual(copy.id, original.id)
440+
441+
def test_unlink_record_with_sparse_fields(self):
442+
"""Test deleting a record with sparse fields."""
443+
record = self.env["sparse_fields_jsonb.test"].create({"integer": 42})
444+
record_id = record.id
445+
446+
record.unlink()
447+
448+
# Should not exist anymore
449+
self.env.cr.execute(
450+
"""
451+
SELECT id FROM sparse_fields_jsonb_test WHERE id = %s
452+
""",
453+
(record_id,),
454+
)
455+
self.assertIsNone(self.env.cr.fetchone())
456+
457+
def test_float_precision_in_sparse_field(self):
458+
"""Test float precision is preserved in JSONB storage."""
459+
record = self.env["sparse_fields_jsonb.test"].create(
460+
{"float_field": 3.141592653589793}
461+
)
462+
463+
record.flush_recordset()
464+
record.invalidate_recordset()
465+
466+
# Re-read from database
467+
record = self.env["sparse_fields_jsonb.test"].browse(record.id)
468+
self.assertAlmostEqual(record.float_field, 3.141592653589793, places=10)
469+
470+
471+
class TestHooks(TransactionCase):
472+
"""Test installation hooks functionality."""
473+
474+
def test_drop_gin_indexes_on_text_columns_no_indexes(self):
475+
"""Test drop_gin_indexes_on_text_columns when no indexes exist."""
476+
# Should return 0 when no GIN indexes on TEXT columns exist
477+
count = drop_gin_indexes_on_text_columns(self.env.cr)
478+
self.assertEqual(count, 0)
479+
480+
def test_pre_init_hook_runs_without_error(self):
481+
"""Test pre_init_hook executes without errors."""
482+
# pre_init_hook should run without raising exceptions
483+
# even when there are no GIN indexes to drop
484+
pre_init_hook(self.env)
485+
486+
def test_post_init_hook_runs_without_error(self):
487+
"""Test post_init_hook executes without errors."""
488+
# post_init_hook should run without raising exceptions
489+
# even when there are no columns to migrate
490+
post_init_hook(self.env)
491+
492+
def test_post_init_hook_with_jsonb_column(self):
493+
"""Test post_init_hook handles existing JSONB columns."""
494+
# Create a test table with a JSONB column matching the pattern
495+
self.env.cr.execute(
496+
"""
497+
CREATE TABLE IF NOT EXISTS test_hook_table (
498+
id SERIAL PRIMARY KEY,
499+
x_custom_json_test JSONB
500+
)
501+
"""
502+
)
503+
504+
# Run post_init_hook - should not fail
505+
post_init_hook(self.env)
506+
507+
# Check if GIN index was created
508+
self.env.cr.execute(
509+
"""
510+
SELECT indexname FROM pg_indexes
511+
WHERE tablename = 'test_hook_table'
512+
AND indexname LIKE '%gin%'
513+
"""
514+
)
515+
result = self.env.cr.fetchone()
516+
self.assertIsNotNone(result)
517+
518+
# Cleanup
519+
self.env.cr.execute("DROP TABLE IF EXISTS test_hook_table")
520+
521+
def test_gin_index_creation_on_jsonb(self):
522+
"""Test that GIN indexes can be created on JSONB columns."""
523+
# Create a test table
524+
self.env.cr.execute(
525+
"""
526+
CREATE TABLE IF NOT EXISTS test_gin_jsonb (
527+
id SERIAL PRIMARY KEY,
528+
data JSONB
529+
)
530+
"""
531+
)
532+
533+
# Create GIN index
534+
self.env.cr.execute(
535+
"""
536+
CREATE INDEX IF NOT EXISTS idx_test_gin_jsonb_data
537+
ON test_gin_jsonb USING GIN (data)
538+
"""
539+
)
540+
541+
# Verify index exists
542+
self.env.cr.execute(
543+
"""
544+
SELECT indexname FROM pg_indexes
545+
WHERE tablename = 'test_gin_jsonb'
546+
AND indexname = 'idx_test_gin_jsonb_data'
547+
"""
548+
)
549+
result = self.env.cr.fetchone()
550+
self.assertIsNotNone(result)
551+
552+
# Cleanup
553+
self.env.cr.execute("DROP TABLE IF EXISTS test_gin_jsonb")
554+
555+
556+
class TestSerializedJsonbField(TransactionCase):
557+
"""Test SerializedJsonb field class properties."""
558+
559+
def test_field_type(self):
560+
"""Test that SerializedJsonb has correct type."""
561+
field = SerializedJsonb()
562+
self.assertEqual(field.type, "serialized")
563+
564+
def test_field_column_type(self):
565+
"""Test that SerializedJsonb uses JSONB column type."""
566+
field = SerializedJsonb()
567+
self.assertEqual(field.column_type, ("jsonb", "jsonb"))
568+
569+
def test_field_prefetch(self):
570+
"""Test that SerializedJsonb is not prefetched by default."""
571+
field = SerializedJsonb()
572+
self.assertFalse(field.prefetch)
573+
574+
def test_serialized_class_is_replaced(self):
575+
"""Test that fields.Serialized is now SerializedJsonb."""
576+
self.assertIs(fields.Serialized, SerializedJsonb)

0 commit comments

Comments
 (0)