mito icon indicating copy to clipboard operation
mito copied to clipboard

Unbound slot error

Open kinofsolmorrow opened this issue 4 years ago • 9 comments

Hi,

I have created 3 tables. users, courses and user-courses to implement a many-to-many relationship between the users and courses. When I try to access the user or courses object via the user-courses entry, which I got with find-dao from the database, I get an unbound slot error. Since I'm pretty new to Lisp (this is my first real application), I don't know if this behaviour is intended or actually a bug?

Simplified code to reproduce:

(defparameter *connection*
  (mito:connect-toplevel :sqlite3 :database-name "mitotest.db"))

(mito:deftable users ()
  ((name :col-type (:varchar 64))
   (email-address :col-type (:varchar 128))))

(mito:deftable courses ()
  ((name :col-type (:varchar 128))))

(mito:deftable user-courses ()
  ((user :col-type users)
   (course :col-type courses)
   (selected :col-type :bytea)))

(mapcar #'mito:ensure-table-exists '(users courses user-courses))


(let* ((user (make-instance 'users :name "Me" :email-address "[email protected]"))
       (course (make-instance 'courses :name "Test Course"))
       (user-course (make-instance 'user-courses :user user :course course :selected 0)))
  (mapcar #'mito:insert-dao (list user course user-course)))


(defvar *my-course* (user-courses-course (mito:find-dao 'user-courses :id 1)))

Results in a unbound-slot error when trying to access the course from the user-courses object.

kinofsolmorrow avatar Apr 13 '20 17:04 kinofsolmorrow

Hi, This simplified example works fine:

(let* ((user (make-instance 'users :name "Me" :email-address "[email protected]"))
       (course (make-instance 'courses :name "Test Course"))
       (user-course (make-instance 'user-courses :user user :course course :selected 0)))
  (print (user-courses-course user-course)))

(your example lacks the lines to create the DB)

What does the find-dao return? You could inspect the result.

You can ask on Stack Overflow.

ps: I'd use (list user …) instead of backquote and comma.

vindarel avatar Apr 14 '20 09:04 vindarel

Hi Vindarel, thanks for checking out the issue!

Of course I have created the DB in my actual application, this was just very simplified. :)

Yeah, I know that it works when I directly bind the objects returned from make-instance, but what is the point of saving it in a DB when I can't access them anymore afterwards? (type-of ...) says that a user-courses object is returned by find-dao, so that is correct. When I inspect the object, I see that course and user are unbound, but course-id and user-id are bound to the corresponding object ids. Therefore, I can't access the user and course slot.

In my opinion I should retrieve a user/course object when trying to access these slots (just like with the freshly created object from make-instance), correct?

And thanks for the tip with (list ...), you are right, definitely more readable. ;)

kinofsolmorrow avatar Apr 14 '20 09:04 kinofsolmorrow

Okay, I just found out that I can retrieve the user id and the course id via user-courses-course-id and user-courses-user-id. Therefore I am able to retrieve the object like that and I suppose that user-courses-course doesn't work by design?

(defvar *my-course* (mito-find-dao 'courses :id (user-courses-course-id
                                                 (mito:find-dao 'user-courses :id 1))))

Works.

kinofsolmorrow avatar Apr 14 '20 10:04 kinofsolmorrow

Of course I have created the DB in my actual application, this was just very simplified. :)

just saying your example isn't totally self-contained and ready to test for the lazy. I'll add

(defparameter *connection*
  (mito:connect-toplevel :sqlite3 :database-name "mitotest.db"))

and

(loop for table in '(users courses user-courses)
     do (mapc #'mito:execute-sql (mito:table-definition table)))

In my opinion I should retrieve a user/course object when trying to access these slots (just like with the freshly created object from make-instance), correct?

agreed, so

user-courses-course doesn't work by design?

AFAIK, it should. I don't understand either why the newly-created object is ok, and find-dao doesn't bind user or course:

;; after manual creation:
(mito:insert-dao *USER-COURSE*)
#<USER-COURSES {1003473FA3}>
CL-USER> (inspect *)

The object is a STANDARD-OBJECT of type USER-COURSES.
0. CREATED-AT: @2020-04-15T10:52:43.720762+02:00
1. UPDATED-AT: @2020-04-15T10:52:43.720762+02:00
2. SYNCED: T
3. ID: 2
4. USER: #<USERS {1003406DC3}>
5. USER-ID: "unbound"
6. COURSE: #<COURSES {1003423BD3}>
7. COURSE-ID: "unbound"
8. SELECTED: 0

CL-USER> (mito:find-dao 'user-courses :id 2)
#<USER-COURSES {10034AB1B3}>
CL-USER> (inspect *)

The object is a STANDARD-OBJECT of type USER-COURSES.
0. CREATED-AT: @2020-04-15T10:52:43.720762+02:00
1. UPDATED-AT: @2020-04-15T10:52:43.720762+02:00
2. SYNCED: T
3. ID: 2
4. USER: "unbound"
5. USER-ID: 2
6. COURSE: "unbound"
7. COURSE-ID: 2
8. SELECTED: 0

vindarel avatar Apr 15 '20 09:04 vindarel

just saying your example isn't totally self-contained and ready to test for the lazy.

Alright, I think I self-contained it now.

AFAIK, it should. I don't understand either why the newly-created object is ok, and find-dao doesn't bind user or course.

Okay, thank you ... then I'll leave the issue open.

kinofsolmorrow avatar Apr 15 '20 18:04 kinofsolmorrow

I have again been puzzled by that. As I inspect or describe an object, Mito says that a relation (user or course) is unbound. But: I can very well access it with its accessor. (I'd like to check this doesn't fire a DB query, just to be sure…)

So I tried again your example, and it works if you add an accessor.

Given this user-course table:

(mito:deftable user-courses ()
  ((user :col-type users)
   (course :col-type courses)
   (selected :col-type :bytea)))

You tried to call user-courses-course (supposedly automatically generated by Mito, and the symbol is indeed present with the autocompletion) on a user-courses object ((mito:find-dao 'user-courses :id 1))), but we got an error:

The slot BOOKSHOPS.MODELS::COURSE is unbound in the object #<USER-COURSES {10084B8CB3}>.

Now it works if you define an accessor:

(mito:deftable user-courses ()
  ((user :col-type users)
   (course :col-type courses
           :accessor course) ;; <= addition
   (selected :col-type :bytea)))

and use it:

(course (mito:find-dao 'user-courses :id 1))
#<COURSES {1008427043}>

vindarel avatar Mar 25 '21 15:03 vindarel

Come into similar issues, here is major part of my code:

(define-table file ()
    ;;omit since not important
  )

(define-table test ()
  ((file-dao :col-type file
	    :initarg :file-dao
	    :accessor file-dao)))
(with-slots (file-dao) (find-dao 'test)

  ;;omit since not important
  )

And, as you can see, the slot file-dao is unbound.

So I looked into the source code and found the followings:

  1. When select-dao (which is behind find-dao and retrieve-dao) make-dao-instance calling select-by-sql for an object you get from db, it simply won't initialize slots of which col-type points to anohter class, leaving the value of that slot unbound
  2. Instead, I suppose that Mito create another method for read when you deftable, using something like file-dao-id to retrieve the corresponding file object, I believe it have something to do with add-relational-readers. Update: This method may depends on :accessor, case
(define-table test123 ()
  ((file-dao :col-type file
	     :initarg :file-dao-1
	     :accessor file-dao-2)))
;;only file-dao-2 works
(file-dao-2 (find-dao 'test123))

(mapcar (lambda (slot)
	  (c2mop:slot-definition-readers slot))
	(c2mop:class-direct-slots (class-of (find-dao 'test123))))
;; this can provide useful info too.

And according to comments in this issues:

  1. I suppose that the user-courses-course depends on the value of slot coures. Since course work with :accessor. I suppose we can confirm that once what is :conc-name been known. Update:HyperSpec says that

:conc-name ... prefixing of names of reader (or access) functions for defstrut.

I suppose it do the same work for defclass, and since the value of slot is unbound, then prefix-name is unbound too.

BTW,

(defclass te ()
  ((a)))
(a (make-instance 'te))

will give The function FOO::A is undefined.(Since it has no :accessor)

Since my goal is simply to use with-slots, I did this silly work around: (with-accessors should fill my needed, but I'm lazy and don't want to type more.)

(defun find-unbound-slot (obj)
  "Return unbound slots and workaround access read method to obj"
  (let ((direct-slots (mapcar #'c2mop:slot-definition-name
			      (c2mop:class-direct-slots (class-of obj)))))
    (mapc (lambda (slot)
	    (when (slot-boundp obj slot)
	      (alexandria:removef direct-slots slot)))
	  direct-slots)
    (remove-duplicates direct-slots)))

(defmacro select-dao (class &body clauses)
  (with-gensyms (sql clause results include-classes foreign-class dao unbound-slot value)
    (once-only (class)
      `(#+sb-package-locks locally #+sb-package-locks (declare (sb-ext:disable-package-locks sxql:where))
        #-sb-package-locks cl-package-locks:with-packages-unlocked #-sb-package-locks (sxql)
        (progn
          (setf ,class (ensure-class ,class))
          (let* ((sxql:*sql-symbol-conversion* #'unlispify)
                 (,sql
                   (sxql:select :*
                                (sxql:from (sxql:make-sql-symbol (table-name ,class)))))
                 (,include-classes '()))
            (macrolet ((where (expression)
                         `(sxql:make-clause :where ,(expand-op expression ',class))))
              ;; ignore compiler-note when includes is not used
              #+sbcl (declare (sb-ext:muffle-conditions sb-ext:code-deletion-note))
              (flet ((includes (&rest classes)
                       (setf ,include-classes (mapcar #'ensure-class classes))
                       nil))
                (dolist (,clause (list ,@clauses))
                  (when ,clause
                    (add-child ,sql ,clause)))
                (let ((,results (select-by-sql ,class ,sql)))
                  (dolist (,foreign-class ,include-classes)
                    (include-foreign-objects ,foreign-class ,results))
		  (alexandria:when-let (,unbound-slot (find-unbound-slot (first ,results)))
		    (dolist (,dao ,results)
		      (dolist (,clause ,unbound-slot)
			(alexandria:when-let* (,value (funcall ,clause ,dao))
			  (setf (,clause ,dao) ,value)))))
                  (values ,results ,sql))))))))))

There are multiple bugs in my work around. My thought is to (setf unbound-slot obj) before it returned from selec-dao. Of course, this wll not handle some case like slot that has no :accessor There should be better solution for this issues.

C-Entropy avatar Mar 26 '21 11:03 C-Entropy

(mito:deftable user-courses ()
  ((user :col-type users)
   (course :col-type courses)
   (selected :col-type :bytea)))

You tried to call user-courses-course (supposedly automatically generated by Mito, and the symbol is indeed present with the autocompletion) on a user-courses object ((mito:find-dao 'user-courses :id 1))), but we got an error:

Seems auto-generated accessors won't work as intended? Need to look into it.

fukamachi avatar Mar 30 '21 03:03 fukamachi

This seems to happen on Arch, but not MacOS (both with SBCL 2.2.6). Very peculiar.

Simponic avatar Jul 10 '22 23:07 Simponic