blog Virtual Tables and Django Foreignkeys

Tags:
django sqlite

I have been continuing with my virtual table implementation, though taking a short break to do some more integration on the Django side.

I have an ideas site that I use as a kind of wiki to keep track of various ideas.

# the default model I'm using from my virtual table integration
class FrontmatterModel(models.Model):
    inode = models.PositiveIntegerField(primary_key=True, editable=False)
    path = models.FilePathField(unique=True, editable=False)
    slug = models.SlugField()

    content = models.TextField()
    excerpt = models.TextField(blank=True)

    metadata = models.JSONField()

    class Meta:
        abstract = True

# Custom model for my idea object itself
# These also exist in the `metadata` json frontmatter, but putting them here
# makes them nicer to deal with from Django
class Idea(FrontmatterModel):
    title = models.CharField(max_length=128)

    status = models.ForeignKey(Status, on_delete=models.SET_NULL, null=True, db_column="dir")

    homepage = models.URLField(blank=True)
    repository = models.URLField(blank=True)
    focus = models.BooleanField(default=False)
    date = models.DateTimeField(default=timezone.now)

    class Meta:
        required_db_vendor = "markdowndb"
        managed = False

The model ultimately gets sent to the virtual table backend with a statement that looks like this

f"""
CREATE VIRTUAL TABLE IF NOT EXISTS temp.{app_model._meta.db_table}
USING {connection.vendor}(schema='{sql}', path='{model_path}');
"""

Where our schema sql looks something like this

CREATE TABLE "ideas_idea" (
	"inode" integer unsigned NOT NULL PRIMARY KEY CHECK ("inode" >= 0),
	"path" varchar(100) NOT NULL UNIQUE,
	"slug" varchar(50) NOT NULL,
	"content" text NOT NULL,
	"excerpt" text NOT NULL,
	"metadata" text NOT NULL CHECK (
		(
			JSON_VALID("metadata")
			OR "metadata" IS NULL
		)
	),
	"title" varchar(128) NOT NULL,
	"dir" varchar(50) NULL REFERENCES "ideas_status" ("slug") DEFERRABLE INITIALLY DEFERRED,
	"homepage" varchar(200) NOT NULL,
	"repository" varchar(200) NOT NULL,
	"focus" bool NOT NULL,
	"date" DATETIME NOT NULL
)

Now that I have my Ideas table reading from my markdown files, I wanted to create a Tag model that I could use as a filter.

class Tag(models.Model):
    name = models.SlugField(unique=True)
    slug = models.SlugField(unique=True)

    # Normally I would add this to my Ideas model but since it is managed=False
    # it get skipped over in migrations.
    idea_set = models.ManyToManyField(Idea, db_constraint=False)

Originally, I created this without db_constraint, which means Django generates foreign constraints on each field.

CREATE TABLE "ideas_tag_idea_set" (
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
    "tag_id" bigint NOT NULL REFERENCES "ideas_tag" ("id") DEFERRABLE INITIALLY DEFERRED,
    "idea_id" integer NOT NULL REFERENCES "ideas_idea" ("inode") DEFERRABLE INITIALLY DEFERRED
    )

This resulted in a error with some of my queries.

django.db.utils.OperationalError: no such table: main.ideas_idea

Since I am currently creating these in the temp. schema, the table is re-created for each new connection. Due to this, I need to ensure that even when using ForeignKey or ManyToMany fields, that Django does not automatically add constraints or they will fail.

Using db_constraints=False generates the following table

CREATE TABLE "ideas_tag_idea_set" (
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
    "tag_id" bigint NOT NULL,
    "idea_id" integer NOT NULL
    )

Instead of db_constraint I could use a through model for my idea_set though I would still need to add db_constraint to the idea_id field. This would let me check the constraints on the tag side, without worrying about errors on the idea side.

CREATE TABLE "ideas_tag_idea_set" (
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
    "tag_id" bigint NOT NULL REFERENCES "ideas_tag" ("id") DEFERRABLE INITIALLY DEFERRED,
    "idea_id" integer NOT NULL
    )

With my models in place, my Tag model is a kind of cache that I can update with a script similar to the following.

tags = collections.defaultdict(list)
for idea in models.Idea.objects.all():
    for tag in idea.metadata.get("tags", []):
        tags[tag].append(idea)

for slug in tags:
    tag, _created = models.Tag.objects.get_or_create(slug=slug, defaults={"name": slug})
    tag.idea_set.set(tags[slug])

In the future, I want to see if I can sort out the differences between main. and temp. schema for the virtual table so that foreign keys can be properly handled but this seems to be working for now.